From deeb84ae5a545684d13fc902fbe435073bafe21a Mon Sep 17 00:00:00 2001 From: nkeywal Date: Mon, 31 Dec 2012 09:50:27 +0000 Subject: [PATCH] HBASE-7390 Add extra test cases for assignement on the region server and fix the related issues git-svn-id: https://svn.apache.org/repos/asf/hbase/trunk@1427064 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/hadoop/hbase/client/HTable.java | 7 +- .../hbase/master/AssignmentManager.java | 5 +- .../hbase/regionserver/HRegionServer.java | 299 +++++++++------ .../regionserver/RegionServerServices.java | 4 +- .../handler/CloseRegionHandler.java | 12 +- .../handler/OpenRegionHandler.java | 150 ++++++-- .../hadoop/hbase/zookeeper/ZKAssign.java | 95 +++-- .../apache/hadoop/hbase/zookeeper/ZKUtil.java | 8 +- .../hadoop/hbase/master/MockRegionServer.java | 3 +- .../TestRegionServerNoMaster.java | 356 ++++++++++++++++++ .../handler/TestOpenRegionHandler.java | 2 +- .../hbase/util/MockRegionServerServices.java | 12 +- 12 files changed, 758 insertions(+), 195 deletions(-) create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestRegionServerNoMaster.java diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/client/HTable.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/client/HTable.java index 7be2386db9e..f5192d952d2 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/client/HTable.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/client/HTable.java @@ -241,7 +241,6 @@ public class HTable implements HTableInterface { /** * setup this HTable's parameter based on the passed configuration - * @param conf */ private void finishSetup() throws IOException { this.connection.locateRegion(tableName, HConstants.EMPTY_START_ROW); @@ -343,11 +342,10 @@ public class HTable implements HTableInterface { } /** - * Finds the region on which the given row is being served. + * Finds the region on which the given row is being served. Does not reload the cache. * @param row Row to find. * @return Location of the row. * @throws IOException if a remote or network exception occurs - * @deprecated use {@link #getRegionLocation(byte [], boolean)} instead */ public HRegionLocation getRegionLocation(final byte [] row) throws IOException { @@ -357,8 +355,7 @@ public class HTable implements HTableInterface { /** * Finds the region on which the given row is being served. * @param row Row to find. - * @param reload whether or not to reload information or just use cached - * information + * @param reload true to reload information or false to use cached information * @return Location of the row. * @throws IOException if a remote or network exception occurs */ diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java index 0d29a0a1b6c..f69afe9f692 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java @@ -162,8 +162,10 @@ public class AssignmentManager extends ZooKeeperListener { * that ServerShutdownHandler can be fully enabled and re-assign regions * of dead servers. So that when re-assignment happens, AssignmentManager * has proper region states. + * + * Protected to ease testing. */ - final AtomicBoolean failoverCleanupDone = new AtomicBoolean(false); + protected final AtomicBoolean failoverCleanupDone = new AtomicBoolean(false); /** * Constructs a new assignment manager. @@ -1461,6 +1463,7 @@ public class AssignmentManager extends ZooKeeperListener { return; } // This never happens. Currently regionserver close always return true. + // Todo; this can now happen (0.96) if there is an exception in a coprocessor LOG.warn("Server " + server + " region CLOSE RPC returned false for " + region.getRegionNameAsString()); } catch (Throwable t) { diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java index fb3f51cc767..6c07246995a 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java @@ -52,6 +52,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.management.ObjectName; +import com.google.protobuf.Message; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.classification.InterfaceAudience; @@ -213,7 +214,6 @@ import org.cliffc.high_scale_lib.Counter; import com.google.common.base.Function; import com.google.protobuf.ByteString; -import com.google.protobuf.Message; import com.google.protobuf.RpcController; import com.google.protobuf.ServiceException; @@ -240,7 +240,7 @@ public class HRegionServer implements ClientProtocol, //RegionName vs current action in progress //true - if open region action in progress //false - if close region action in progress - protected final ConcurrentSkipListMap regionsInTransitionInRS = + protected final ConcurrentMap regionsInTransitionInRS = new ConcurrentSkipListMap(Bytes.BYTES_COMPARATOR); protected long maxScannerResultSize; @@ -443,7 +443,7 @@ public class HRegionServer implements ClientProtocol, this.isOnline = false; checkCodecs(this.conf); - // do we use checksum verfication in the hbase? If hbase checksum verification + // do we use checksum verification in the hbase? If hbase checksum verification // is enabled, then we automatically switch off hdfs checksum verification. this.useHBaseChecksum = conf.getBoolean( HConstants.HBASE_CHECKSUM_VERIFICATION, false); @@ -665,7 +665,7 @@ public class HRegionServer implements ClientProtocol, if (rpcArgClass == null || from.getRequest().isEmpty()) { return HConstants.NORMAL_QOS; } - Object deserializedRequestObj = null; + Object deserializedRequestObj; //check whether the request has reference to Meta region try { Method parseFrom = methodMap.get("parseFrom").get(rpcArgClass); @@ -960,7 +960,7 @@ public class HRegionServer implements ClientProtocol, //fsOk flag may be changed when closing regions throws exception. if (!this.killed && this.fsOk) { - closeWAL(abortRequested ? false : true); + closeWAL(!abortRequested); } // Make sure the proxy is down. @@ -1106,7 +1106,7 @@ public class HRegionServer implements ClientProtocol, && !closedRegions.contains(hri.getEncodedName())) { closedRegions.add(hri.getEncodedName()); // Don't update zk with this close transition; pass false. - closeRegion(hri, abort, false); + closeRegionIgnoreErrors(hri, abort); } } // No regions in RIT, we could stop waiting now. @@ -1167,7 +1167,7 @@ public class HRegionServer implements ClientProtocol, this.serverNameFromMasterPOV.getHostname()); continue; } - String value = e.getValue().toString(); + String value = e.getValue(); if (LOG.isDebugEnabled()) { LOG.debug("Config from master: " + key + "=" + value); } @@ -1440,7 +1440,7 @@ public class HRegionServer implements ClientProtocol, } /* - * Start maintanence Threads, Server, Worker and lease checker threads. + * Start maintenance Threads, Server, Worker and lease checker threads. * Install an UncaughtExceptionHandler that calls abort of RegionServer if we * get an unhandled exception. We cannot set the handler on all threads. * Server's internal Listener thread is off limits. For Server, if an OOME, it @@ -1892,8 +1892,8 @@ public class HRegionServer implements ClientProtocol, } finally { this.lock.writeLock().unlock(); } - if (meta != null) closeRegion(meta.getRegionInfo(), abort, false); - if (root != null) closeRegion(root.getRegionInfo(), abort, false); + if (meta != null) closeRegionIgnoreErrors(meta.getRegionInfo(), abort); + if (root != null) closeRegionIgnoreErrors(root.getRegionInfo(), abort); } /** @@ -1909,7 +1909,7 @@ public class HRegionServer implements ClientProtocol, HRegion r = e.getValue(); if (!r.getRegionInfo().isMetaTable() && r.isAvailable()) { // Don't update zk with this close transition; pass false. - closeRegion(r.getRegionInfo(), abort, false); + closeRegionIgnoreErrors(r.getRegionInfo(), abort); } } } finally { @@ -1984,7 +1984,7 @@ public class HRegionServer implements ClientProtocol, }); // Copy over all regions. Regions are sorted by size with biggest first. for (HRegion region : this.onlineRegions.values()) { - sortedRegions.put(Long.valueOf(region.memstoreSize.get()), region); + sortedRegions.put(region.memstoreSize.get(), region); } return sortedRegions; } @@ -2106,7 +2106,7 @@ public class HRegionServer implements ClientProtocol, } - public ConcurrentSkipListMap getRegionsInTransitionInRS() { + public ConcurrentMap getRegionsInTransitionInRS() { return this.regionsInTransitionInRS; } @@ -2267,7 +2267,7 @@ public class HRegionServer implements ClientProtocol, for (HRegion region: regions) { coprocessors.addAll(region.getCoprocessorHost().getCoprocessors()); } - return coprocessors.toArray(new String[0]); + return coprocessors.toArray(new String[coprocessors.size()]); } /** @@ -2367,70 +2367,103 @@ public class HRegionServer implements ClientProtocol, } } - protected void checkIfRegionInTransition(byte[] regionEncodedName, - String currentAction) throws RegionAlreadyInTransitionException { - if (this.regionsInTransitionInRS.containsKey(regionEncodedName)) { - boolean openAction = this.regionsInTransitionInRS.get(regionEncodedName); - // The below exception message will be used in master. - throw new RegionAlreadyInTransitionException("Received:" + currentAction + - " for the region:" + Bytes.toString(regionEncodedName) + - " ,which we are already trying to " + - (openAction ? OPEN : CLOSE)+ "."); + + /** + * Try to close the region, logs a warning on failure but continues. + * @param region Region to close + */ + private void closeRegionIgnoreErrors(HRegionInfo region, final boolean abort) { + try { + if (!closeRegion(region.getEncodedName(), abort, false, -1, null)) { + LOG.warn("Failed to close " + region.getRegionNameAsString() + + " - ignoring and continuing"); + } + } catch (NotServingRegionException e) { + LOG.warn("Failed to close " + region.getRegionNameAsString() + + " - ignoring and continuing", e); } } /** - * @param region Region to close - * @param abort True if we are aborting - * @param zk True if we are to update zk about the region close; if the close - * was orchestrated by master, then update zk. If the close is being run by - * the regionserver because its going down, don't update zk. - * @return True if closed a region. - */ - protected boolean closeRegion(HRegionInfo region, final boolean abort, - final boolean zk) { - return closeRegion(region, abort, zk, -1, null); - } + * Close asynchronously a region, can be called from the master or internally by the regionserver + * when stopping. If called from the master, the region will update the znode status. + * + *

+ * If an opening was in progress, this method will cancel it, but will not start a new close. The + * coprocessors are not called in this case. A NotServingRegionException exception is thrown. + *

- /** - * @param region Region to close + *

+ * If a close was in progress, this new request will be ignored, and an exception thrown. + *

+ * + * @param encodedName Region to close * @param abort True if we are aborting * @param zk True if we are to update zk about the region close; if the close * was orchestrated by master, then update zk. If the close is being run by * the regionserver because its going down, don't update zk. - * @param versionOfClosingNode - * the version of znode to compare when RS transitions the znode from + * @param versionOfClosingNode the version of znode to compare when RS transitions the znode from * CLOSING state. * @return True if closed a region. + * @throws NotServingRegionException if the region is not online or if a close + * request in in progress. */ - protected boolean closeRegion(HRegionInfo region, final boolean abort, - final boolean zk, final int versionOfClosingNode, ServerName sn) { + protected boolean closeRegion(String encodedName, final boolean abort, + final boolean zk, final int versionOfClosingNode, final ServerName sn) + throws NotServingRegionException { //Check for permissions to close. - HRegion actualRegion = this.getFromOnlineRegions(region.getEncodedName()); + final HRegion actualRegion = this.getFromOnlineRegions(encodedName); if ((actualRegion != null) && (actualRegion.getCoprocessorHost() != null)) { try { actualRegion.getCoprocessorHost().preClose(false); } catch (IOException exp) { - LOG.warn("Unable to close region", exp); + LOG.warn("Unable to close region: the coprocessor launched an error ", exp); return false; } } - if (this.regionsInTransitionInRS.containsKey(region.getEncodedNameAsBytes())) { - LOG.warn("Received close for region we are already opening or closing; " + - region.getEncodedName()); - return false; + final Boolean previous = this.regionsInTransitionInRS.putIfAbsent(encodedName.getBytes(), + Boolean.FALSE); + + if (Boolean.TRUE.equals(previous)) { + LOG.info("Received CLOSE for the region:" + encodedName + " , which we are already " + + "trying to OPEN. Cancelling OPENING."); + if (!regionsInTransitionInRS.replace(encodedName.getBytes(), previous, Boolean.FALSE)){ + // The replace failed. That should be an exceptional case, but theoretically it can happen. + // We're going to try to do a standard close then. + LOG.warn("The opening for region " + encodedName + " was done before we could cancel it." + + " Doing a standard close now"); + return closeRegion(encodedName, abort, zk, versionOfClosingNode, sn); + } else { + LOG.info("The opening previously in progress has been cancelled by a CLOSE request."); + // The master deletes the znode when it receives this exception. + throw new NotServingRegionException("The region " + encodedName + + " was opening but not yet served. Opening is cancelled."); + } + } else if (Boolean.FALSE.equals(previous)) { + LOG.info("Received CLOSE for the region: " + encodedName + + " ,which we are already trying to CLOSE"); + // The master deletes the znode when it receives this exception. + throw new NotServingRegionException("The region " + encodedName + + " was already closing. New CLOSE request is ignored."); } - this.regionsInTransitionInRS.putIfAbsent(region.getEncodedNameAsBytes(), false); - CloseRegionHandler crh = null; - if (region.isRootRegion()) { - crh = new CloseRootHandler(this, this, region, abort, zk, - versionOfClosingNode); - } else if (region.isMetaRegion()) { - crh = new CloseMetaHandler(this, this, region, abort, zk, - versionOfClosingNode); + + if (actualRegion == null){ + LOG.error("Received CLOSE for a region which is not online, and we're not opening."); + this.regionsInTransitionInRS.remove(encodedName.getBytes()); + // The master deletes the znode when it receives this exception. + throw new NotServingRegionException("The region " + encodedName + + " is not online, and is not opening."); + } + + CloseRegionHandler crh; + final HRegionInfo hri = actualRegion.getRegionInfo(); + if (hri.isRootRegion()) { + crh = new CloseRootHandler(this, this, hri, abort, zk, versionOfClosingNode); + } else if (hri.isMetaRegion()) { + crh = new CloseMetaHandler(this, this, hri, abort, zk, versionOfClosingNode); } else { - crh = new CloseRegionHandler(this, this, region, abort, zk, versionOfClosingNode, sn); + crh = new CloseRegionHandler(this, this, hri, abort, zk, versionOfClosingNode, sn); } this.service.submit(crh); return true; @@ -2517,7 +2550,7 @@ public class HRegionServer implements ClientProtocol, protected Throwable cleanup(final Throwable t, final String msg) { // Don't log as error if NSRE; NSRE is 'normal' operation. if (t instanceof NotServingRegionException) { - LOG.debug("NotServingRegionException; " + t.getMessage()); + LOG.debug("NotServingRegionException; " + t.getMessage()); return t; } if (msg == null) { @@ -2682,7 +2715,7 @@ public class HRegionServer implements ClientProtocol, if (region.getCoprocessorHost() != null) { exists = region.getCoprocessorHost().postExists(clientGet, exists); } - existence = Boolean.valueOf(exists); + existence = exists; } } } @@ -2717,7 +2750,7 @@ public class HRegionServer implements ClientProtocol, if (!region.getRegionInfo().isMetaTable()) { cacheFlusher.reclaimMemStoreMemory(); } - Integer lock = null; + Integer lock; Result r = null; Boolean processed = null; MutateType type = mutate.getMutateType(); @@ -2750,7 +2783,7 @@ public class HRegionServer implements ClientProtocol, result = region.getCoprocessorHost().postCheckAndPut(row, family, qualifier, compareOp, comparator, put, result); } - processed = Boolean.valueOf(result); + processed = result; } } else { region.put(put, lock); @@ -2779,7 +2812,7 @@ public class HRegionServer implements ClientProtocol, result = region.getCoprocessorHost().postCheckAndDelete(row, family, qualifier, compareOp, comparator, delete, result); } - processed = Boolean.valueOf(result); + processed = result; } } else { region.delete(delete, lock, delete.getWriteToWAL()); @@ -3088,7 +3121,7 @@ public class HRegionServer implements ClientProtocol, region.releaseRowLock(r); this.leases.cancelLease(lockName); LOG.debug("Row lock " + lockId - + " has been explicitly released by client"); + + " has been explicitly released by client"); return UnlockRowResponse.newBuilder().build(); } catch (Throwable t) { throw convertThrowableToIOE(cleanup(t)); @@ -3286,7 +3319,7 @@ public class HRegionServer implements ClientProtocol, try { HRegion region = getRegion(request.getRegion()); requestCount.increment(); - Set columnFamilies = null; + Set columnFamilies; if (request.getFamilyCount() == 0) { columnFamilies = region.getStores().keySet(); } else { @@ -3328,8 +3361,24 @@ public class HRegionServer implements ClientProtocol, // Region open/close direct RPCs /** - * Open a region on the region server. + * Open asynchronously a region or a set of regions on the region server. * + * The opening is coordinated by ZooKeeper, and this method requires the znode to be created + * before being called. As a consequence, this method should be called only from the master. + *

+ * Different manages states for the region are:

    + *
  • region not opened: the region opening will start asynchronously.
  • + *
  • a close is already in progress: this is considered as an error.
  • + *
  • an open is already in progress: this new open request will be ignored. This is important + * because the Master can do multiple requests if it crashes.
  • + *
  • the region is already opened: this new open request will be ignored./li> + *
+ *

+ *

+ * Bulk assign: If there are more than 1 region to open, it will be considered as a bulk assign. + * For a single region opening, errors are sent through a ServiceException. For bulk assign, + * errors are put in the response as FAILED_OPENING. + *

* @param controller the RPC controller * @param request the request * @throws ServiceException @@ -3345,22 +3394,22 @@ public class HRegionServer implements ClientProtocol, } requestCount.increment(); OpenRegionResponse.Builder builder = OpenRegionResponse.newBuilder(); - int regionCount = request.getOpenInfoCount(); - Map htds = - new HashMap(regionCount); - boolean isBulkAssign = regionCount > 1; + final int regionCount = request.getOpenInfoCount(); + final Map htds = new HashMap(regionCount); + final boolean isBulkAssign = regionCount > 1; for (RegionOpenInfo regionOpenInfo : request.getOpenInfoList()) { - HRegionInfo region = HRegionInfo.convert(regionOpenInfo.getRegion()); + final HRegionInfo region = HRegionInfo.convert(regionOpenInfo.getRegion()); + int versionOfOfflineNode = -1; if (regionOpenInfo.hasVersionOfOfflineNode()) { versionOfOfflineNode = regionOpenInfo.getVersionOfOfflineNode(); } + HTableDescriptor htd; try { - checkIfRegionInTransition(region.getEncodedNameAsBytes(), OPEN); - HRegion onlineRegion = getFromOnlineRegions(region.getEncodedName()); - if (null != onlineRegion) { + final HRegion onlineRegion = getFromOnlineRegions(region.getEncodedName()); + if (onlineRegion != null) { //Check if the region can actually be opened. - if( onlineRegion.getCoprocessorHost() != null){ + if (onlineRegion.getCoprocessorHost() != null) { onlineRegion.getCoprocessorHost().preOpen(); } // See HBASE-5094. Cross check with META if still this RS is owning @@ -3373,39 +3422,55 @@ public class HRegionServer implements ClientProtocol, builder.addOpeningState(RegionOpeningState.ALREADY_OPENED); continue; } else { - LOG.warn("The region " + region.getEncodedName() - + " is online on this server but META does not have this server."); + LOG.warn("The region " + region.getEncodedName() + " is online on this server" + + " but META does not have this server - continue opening."); removeFromOnlineRegions(region.getEncodedName(), null); } } LOG.info("Received request to open region: " + region.getRegionNameAsString() + " on " + this.serverNameFromMasterPOV); - HTableDescriptor htd = htds.get(region.getTableNameAsString()); + htd = htds.get(region.getTableNameAsString()); if (htd == null) { htd = this.tableDescriptors.get(region.getTableName()); htds.put(region.getTableNameAsString(), htd); } - this.regionsInTransitionInRS.putIfAbsent( - region.getEncodedNameAsBytes(), true); - // Need to pass the expected version in the constructor. - if (region.isRootRegion()) { - this.service.submit(new OpenRootHandler(this, this, region, htd, - versionOfOfflineNode)); - } else if (region.isMetaRegion()) { - this.service.submit(new OpenMetaHandler(this, this, region, htd, - versionOfOfflineNode)); - } else { - this.service.submit(new OpenRegionHandler(this, this, region, htd, - versionOfOfflineNode)); + + final Boolean previous = this.regionsInTransitionInRS.putIfAbsent( + region.getEncodedNameAsBytes(), Boolean.TRUE); + + if (Boolean.FALSE.equals(previous)) { + // There is a close in progress. We need to mark this open as failed in ZK. + OpenRegionHandler. + tryTransitionFromOfflineToFailedOpen(this, region, versionOfOfflineNode); + + throw new RegionAlreadyInTransitionException("Received OPEN for the region:" + + region.getRegionNameAsString() + " , which we are already trying to CLOSE "); } + + if (Boolean.TRUE.equals(previous)) { + // An open is in progress. This is supported, but let's log this. + LOG.info("Receiving OPEN for the region:" + + region.getRegionNameAsString() + " , which we are already trying to OPEN" + + " - ignoring this new request for this region."); + } + + if (previous == null) { + // If there is no action in progress, we can submit a specific handler. + // Need to pass the expected version in the constructor. + if (region.isRootRegion()) { + this.service.submit(new OpenRootHandler(this, this, region, htd, + versionOfOfflineNode)); + } else if (region.isMetaRegion()) { + this.service.submit(new OpenMetaHandler(this, this, region, htd, + versionOfOfflineNode)); + } else { + this.service.submit(new OpenRegionHandler(this, this, region, htd, + versionOfOfflineNode)); + } + } + builder.addOpeningState(RegionOpeningState.OPENED); - } catch (RegionAlreadyInTransitionException rie) { - LOG.warn("Region is already in transition", rie); - if (isBulkAssign) { - builder.addOpeningState(RegionOpeningState.OPENED); - } else { - throw new ServiceException(rie); - } + } catch (IOException ie) { LOG.warn("Failed opening region " + region.getRegionNameAsString(), ie); if (isBulkAssign) { @@ -3415,6 +3480,7 @@ public class HRegionServer implements ClientProtocol, } } } + return builder.build(); } @@ -3439,31 +3505,22 @@ public class HRegionServer implements ClientProtocol, try { checkOpen(); - String encodedRegionName = - ProtobufUtil.getRegionEncodedName(request.getRegion()); - byte[] encodedName = Bytes.toBytes(encodedRegionName); - HRegion region = getRegionByEncodedName(encodedRegionName); - if(region.getCoprocessorHost() != null){ + final String encodedRegionName = ProtobufUtil.getRegionEncodedName(request.getRegion()); + + // Can be null if we're calling close on a region that's not online + final HRegion region = this.getFromOnlineRegions(encodedRegionName); + if ((region != null) && (region .getCoprocessorHost() != null)) { region.getCoprocessorHost().preClose(false); } - Boolean openAction = regionsInTransitionInRS.get(encodedName); - if (openAction != null) { - if (openAction.booleanValue()) { - regionsInTransitionInRS.replace(encodedName, openAction, Boolean.FALSE); - } - checkIfRegionInTransition(encodedName, CLOSE); - } requestCount.increment(); - LOG.info("Received close region: " + region.getRegionNameAsString() + - ". Version of ZK closing node:" + versionOfClosingNode + + LOG.info("Received close region: " + encodedRegionName + + "Transitioning in ZK: " + (zk ? "yes" : "no") + + ". Version of ZK closing node:" + versionOfClosingNode + ". Destination server:" + sn); - HRegionInfo regionInfo = region.getRegionInfo(); - checkIfRegionInTransition(encodedName, CLOSE); - boolean closed = closeRegion( - regionInfo, false, zk, versionOfClosingNode, sn); - CloseRegionResponse.Builder builder = - CloseRegionResponse.newBuilder().setClosed(closed); + + boolean closed = closeRegion(encodedRegionName, false, zk, versionOfClosingNode, sn); + CloseRegionResponse.Builder builder = CloseRegionResponse.newBuilder().setClosed(closed); return builder.build(); } catch (IOException ie) { throw new ServiceException(ie); @@ -3554,7 +3611,7 @@ public class HRegionServer implements ClientProtocol, store = region.getStore(family); if (store == null) { throw new ServiceException(new IOException("column family " + Bytes.toString(family) + - " does not exist in region " + new String(region.getRegionNameAsString()))); + " does not exist in region " + region.getRegionNameAsString())); } } if (request.hasMajor()) { @@ -3681,7 +3738,7 @@ public class HRegionServer implements ClientProtocol, protected HRegion getRegion( final RegionSpecifier regionSpecifier) throws IOException { return getRegionByEncodedName( - ProtobufUtil.getRegionEncodedName(regionSpecifier)); + ProtobufUtil.getRegionEncodedName(regionSpecifier)); } /** @@ -3757,7 +3814,7 @@ public class HRegionServer implements ClientProtocol, ActionResult result = resultBuilder.build(); int i = 0; for (Mutate m : mutates) { - Mutation mutation = null; + Mutation mutation; if (m.getMutateType() == MutateType.PUT) { mutation = ProtobufUtil.toPut(m); batchContainsPuts = true; @@ -3850,10 +3907,10 @@ public class HRegionServer implements ClientProtocol, } - // This map will containsall the regions that we closed for a move. + // This map will contains all the regions that we closed for a move. // We add the time it was moved as we don't want to keep too old information protected Map> movedRegions = - new ConcurrentHashMap>(3000); + new ConcurrentHashMap>(3000); // We need a timeout. If not there is a risk of giving a wrong information: this would double // the number of network calls instead of reducing them. @@ -3867,8 +3924,8 @@ public class HRegionServer implements ClientProtocol, final Long time = System.currentTimeMillis(); movedRegions.put( - encodedName, - new Pair(time, destination)); + encodedName, + new Pair(time, destination)); } private ServerName getMovedRegion(final String encodedRegionName) { diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RegionServerServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RegionServerServices.java index 8583113fa26..422df5001ad 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RegionServerServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RegionServerServices.java @@ -19,7 +19,7 @@ package org.apache.hadoop.hbase.regionserver; import java.io.IOException; -import java.util.Map; +import java.util.concurrent.ConcurrentMap; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.fs.FileSystem; @@ -79,7 +79,7 @@ public interface RegionServerServices extends OnlineRegions { * Get the regions that are currently being opened or closed in the RS * @return map of regions in transition in this RS */ - public Map getRegionsInTransitionInRS(); + public ConcurrentMap getRegionsInTransitionInRS(); /** * @return Return the FileSystem object used by the regionserver diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/handler/CloseRegionHandler.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/handler/CloseRegionHandler.java index c534f4b8215..b11555a3251 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/handler/CloseRegionHandler.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/handler/CloseRegionHandler.java @@ -126,13 +126,21 @@ public class CloseRegionHandler extends EventHandler { // Check that this region is being served here HRegion region = this.rsServices.getFromOnlineRegions(encodedRegionName); if (region == null) { - LOG.warn("Received CLOSE for region " + name + - " but currently not serving"); + LOG.warn("Received CLOSE for region " + name + " but currently not serving - ignoring"); + if (zk){ + LOG.error("The znode is not modified as we are not serving " + name); + } + // TODO: do better than a simple warning return; } // Close the region try { + if (zk && !ZKAssign.checkClosingState(server.getZooKeeper(), regionInfo, expectedVersion)){ + // bad znode state + return; // We're node deleting the znode, but it's not ours... + } + // TODO: If we need to keep updating CLOSING stamp to prevent against // a timeout if this is long-running, need to spin up a thread? if (region.close(abort) == null) { diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/handler/OpenRegionHandler.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/handler/OpenRegionHandler.java index f2ed2a2942d..2c94060a18e 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/handler/OpenRegionHandler.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/handler/OpenRegionHandler.java @@ -85,30 +85,50 @@ public class OpenRegionHandler extends EventHandler { @Override public void process() throws IOException { + boolean openSuccessful = false; + final String regionName = regionInfo.getRegionNameAsString(); + try { - final String name = regionInfo.getRegionNameAsString(); if (this.server.isStopped() || this.rsServices.isStopping()) { return; } final String encodedName = regionInfo.getEncodedName(); - // Check that this region is not already online - HRegion region = this.rsServices.getFromOnlineRegions(encodedName); + // 3 different difficult situations can occur + // 1) The opening was cancelled. This is an expected situation + // 2) The region was hijacked, we no longer have the znode + // 3) The region is now marked as online while we're suppose to open. This would be a bug. + // Check that this region is not already online + if (this.rsServices.getFromOnlineRegions(encodedName) != null) { + LOG.error("Region " + encodedName + + " was already online when we started processing the opening. " + + "Marking this new attempt as failed"); + tryTransitionFromOfflineToFailedOpen(this.rsServices, regionInfo, this.version); + return; + } + + // Check that we're still supposed to open the region and transition. // If fails, just return. Someone stole the region from under us. - // Calling transitionZookeeperOfflineToOpening initalizes this.version. - if (!transitionZookeeperOfflineToOpening(encodedName, - versionOfOfflineNode)) { - LOG.warn("Region was hijacked? It no longer exists, encodedName=" + - encodedName); + // Calling transitionZookeeperOfflineToOpening initializes this.version. + if (!isRegionStillOpening()){ + LOG.error("Region " + encodedName + " opening cancelled"); + tryTransitionFromOfflineToFailedOpen(this.rsServices, regionInfo, this.version); + return; + } + + if (!transitionZookeeperOfflineToOpening(encodedName, versionOfOfflineNode)) { + LOG.warn("Region was hijacked? Opening cancelled for encodedName=" + encodedName); + // This is a desperate attempt: the znode is unlikely to be ours. But we can't do more. + tryTransitionFromOfflineToFailedOpen(this.rsServices, regionInfo, this.version); return; } // Open region. After a successful open, failures in subsequent // processing needs to do a close as part of cleanup. - region = openRegion(); + HRegion region = openRegion(); if (region == null) { - tryTransitionToFailedOpen(regionInfo); + tryTransitionFromOpeningToFailedOpen(regionInfo); return; } boolean failed = true; @@ -120,37 +140,63 @@ public class OpenRegionHandler extends EventHandler { if (failed || this.server.isStopped() || this.rsServices.isStopping()) { cleanupFailedOpen(region); - tryTransitionToFailedOpen(regionInfo); + tryTransitionFromOpeningToFailedOpen(regionInfo); return; } - if (!transitionToOpened(region)) { + + if (!isRegionStillOpening() || !transitionToOpened(region)) { // If we fail to transition to opened, it's because of one of two cases: // (a) we lost our ZK lease // OR (b) someone else opened the region before us - // In either case, we don't need to transition to FAILED_OPEN state. - // In case (a), the Master will process us as a dead server. In case - // (b) the region is already being handled elsewhere anyway. + // OR (c) someone cancelled the open + // In all cases, we try to transition to failed_open to be safe. cleanupFailedOpen(region); + tryTransitionFromOpeningToFailedOpen(regionInfo); return; } - // One more check to make sure we are opening instead of closing - if (!isRegionStillOpening()) { - LOG.warn("Open region aborted since it isn't opening any more"); - cleanupFailedOpen(region); - return; - } + // We have a znode in the opened state now. We can't really delete it as the master job. + // Transitioning to failed open would create a race condition if the master has already + // acted the transition to opened. + // Cancelling the open is dangerous, because we would have a state where the master thinks + // the region is opened while the region is actually closed. It is a dangerous state + // to be in. For this reason, from now on, we're not going back. There is a message in the + // finally close to let the admin knows where we stand. + // Successful region open, and add it to OnlineRegions this.rsServices.addToOnlineRegions(region); + openSuccessful = true; // Done! Successful region open - LOG.debug("Opened " + name + " on server:" + + LOG.debug("Opened " + regionName + " on server:" + this.server.getServerName()); + + } finally { - this.rsServices.getRegionsInTransitionInRS(). + final Boolean current = this.rsServices.getRegionsInTransitionInRS(). remove(this.regionInfo.getEncodedNameAsBytes()); + + // Let's check if we have met a race condition on open cancellation.... + // A better solution would be to not have any race condition. + // this.rsServices.getRegionsInTransitionInRS().remove( + // this.regionInfo.getEncodedNameAsBytes(), Boolean.TRUE); + // would help, but we would still have a consistency issue to manage with + // 1) this.rsServices.addToOnlineRegions(region); + // 2) the ZK state. + if (openSuccessful) { + if (current == null) { // Should NEVER happen, but let's be paranoid. + LOG.error("Bad state: we've just opened a region that was NOT in transition. Region=" + + regionName + ); + } else if (Boolean.FALSE.equals(current)) { // Can happen, if we're really unlucky. + LOG.error("Race condition: we've finished to open a region, while a close was requested " + + " on region=" + regionName + ". It can be a critical error, as a region that" + + " should be closed is now opened." + ); + } + } } } @@ -226,7 +272,8 @@ public class OpenRegionHandler extends EventHandler { /** * Thread to run region post open tasks. Call {@link #getException()} after * the thread finishes to check for exceptions running - * {@link RegionServerServices#postOpenDeployTasks(HRegion, org.apache.hadoop.hbase.catalog.CatalogTracker, boolean)} + * {@link RegionServerServices#postOpenDeployTasks( + * HRegion, org.apache.hadoop.hbase.catalog.CatalogTracker, boolean)} * . */ static class PostOpenDeployTasksThread extends Thread { @@ -277,10 +324,6 @@ public class OpenRegionHandler extends EventHandler { * @throws IOException */ private boolean transitionToOpened(final HRegion r) throws IOException { - if (!isRegionStillOpening()) { - LOG.warn("Open region aborted since it isn't opening any more"); - return false; - } boolean result = false; HRegionInfo hri = r.getRegionInfo(); final String name = hri.getRegionNameAsString(); @@ -310,11 +353,12 @@ public class OpenRegionHandler extends EventHandler { * @param hri Region we're working on. * @return whether znode is successfully transitioned to FAILED_OPEN state. */ - private boolean tryTransitionToFailedOpen(final HRegionInfo hri) { + private boolean tryTransitionFromOpeningToFailedOpen(final HRegionInfo hri) { boolean result = false; final String name = hri.getRegionNameAsString(); try { - LOG.info("Opening of region " + hri + " failed, marking as FAILED_OPEN in ZK"); + LOG.info("Opening of region " + hri + " failed, transitioning" + + " from OPENING to FAILED_OPEN in ZK, expecting version " + this.version); if (ZKAssign.transitionNode( this.server.getZooKeeper(), hri, this.server.getServerName(), @@ -334,6 +378,43 @@ public class OpenRegionHandler extends EventHandler { return result; } + /** + * Try to transition to open. This function is static to make it usable before creating the + * handler. + * + * This is not guaranteed to succeed, we just do our best. + * + * @param rsServices + * @param hri Region we're working on. + * @param versionOfOfflineNode version to checked. + * @return whether znode is successfully transitioned to FAILED_OPEN state. + */ + public static boolean tryTransitionFromOfflineToFailedOpen(RegionServerServices rsServices, + final HRegionInfo hri, final int versionOfOfflineNode) { + boolean result = false; + final String name = hri.getRegionNameAsString(); + try { + LOG.info("Opening of region " + hri + " failed, transitioning" + + " from OFFLINE to FAILED_OPEN in ZK, expecting version " + versionOfOfflineNode); + if (ZKAssign.transitionNode( + rsServices.getZooKeeper(), hri, + rsServices.getServerName(), + EventType.M_ZK_REGION_OFFLINE, + EventType.RS_ZK_REGION_FAILED_OPEN, + versionOfOfflineNode) == -1) { + LOG.warn("Unable to mark region " + hri + " as FAILED_OPEN. " + + "It's likely that the master already timed out this open " + + "attempt, and thus another RS already has the region."); + } else { + result = true; + } + } catch (KeeperException e) { + LOG.error("Failed transitioning node " + name + " from OFFLINE to FAILED_OPEN", e); + } + return result; + } + + /** * @return Instance of HRegion if successful open else null. */ @@ -379,7 +460,7 @@ public class OpenRegionHandler extends EventHandler { private boolean isRegionStillOpening() { byte[] encodedName = regionInfo.getEncodedNameAsBytes(); Boolean action = rsServices.getRegionsInTransitionInRS().get(encodedName); - return action != null && action.booleanValue(); + return Boolean.TRUE.equals(action); // true means opening for RIT } /** @@ -392,10 +473,6 @@ public class OpenRegionHandler extends EventHandler { */ boolean transitionZookeeperOfflineToOpening(final String encodedName, int versionOfOfflineNode) { - if (!isRegionStillOpening()) { - LOG.warn("Open region aborted since it isn't opening any more"); - return false; - } // TODO: should also handle transition from CLOSED? try { // Initialize the znode version. @@ -405,6 +482,8 @@ public class OpenRegionHandler extends EventHandler { } catch (KeeperException e) { LOG.error("Error transition from OFFLINE to OPENING for region=" + encodedName, e); + this.version = -1; + return false; } boolean b = isGoodVersion(); if (!b) { @@ -436,6 +515,7 @@ public class OpenRegionHandler extends EventHandler { server.abort("Exception refreshing OPENING; region=" + encodedName + ", context=" + context, e); this.version = -1; + return false; } boolean b = isGoodVersion(); if (!b) { diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/zookeeper/ZKAssign.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/zookeeper/ZKAssign.java index 8af93f9eca4..071905c2c98 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/zookeeper/ZKAssign.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/zookeeper/ZKAssign.java @@ -176,7 +176,8 @@ public class ZKAssign { LOG.debug(zkw.prefix("Async create of unassigned node for " + region.getEncodedName() + " with OFFLINE state")); RegionTransition rt = - RegionTransition.createRegionTransition(EventType.M_ZK_REGION_OFFLINE, region.getRegionName(), serverName); + RegionTransition.createRegionTransition( + EventType.M_ZK_REGION_OFFLINE, region.getRegionName(), serverName); String node = getNodeName(zkw, region.getEncodedName()); ZKUtil.asyncCreate(zkw, node, rt.toByteArray(), cb, ctx); } @@ -258,14 +259,14 @@ public class ZKAssign { * of the specified regions transition. * * @param zkw zk reference - * @param regionName opened region to be deleted from zk + * @param encodedRegionName opened region to be deleted from zk * @throws KeeperException if unexpected zookeeper exception * @throws KeeperException.NoNodeException if node does not exist */ public static boolean deleteOpenedNode(ZooKeeperWatcher zkw, - String regionName) + String encodedRegionName) throws KeeperException, KeeperException.NoNodeException { - return deleteNode(zkw, regionName, EventType.RS_ZK_REGION_OPENED); + return deleteNode(zkw, encodedRegionName, EventType.RS_ZK_REGION_OPENED); } /** @@ -283,14 +284,14 @@ public class ZKAssign { * that has died are all set to OFFLINE before being processed. * * @param zkw zk reference - * @param regionName closed region to be deleted from zk + * @param encodedRegionName closed region to be deleted from zk * @throws KeeperException if unexpected zookeeper exception * @throws KeeperException.NoNodeException if node does not exist */ public static boolean deleteOfflineNode(ZooKeeperWatcher zkw, - String regionName) + String encodedRegionName) throws KeeperException, KeeperException.NoNodeException { - return deleteNode(zkw, regionName, EventType.M_ZK_REGION_OFFLINE); + return deleteNode(zkw, encodedRegionName, EventType.M_ZK_REGION_OFFLINE); } /** @@ -309,14 +310,14 @@ public class ZKAssign { * of the specified regions transition to being closed. * * @param zkw zk reference - * @param regionName closed region to be deleted from zk + * @param encodedRegionName closed region to be deleted from zk * @throws KeeperException if unexpected zookeeper exception * @throws KeeperException.NoNodeException if node does not exist */ public static boolean deleteClosedNode(ZooKeeperWatcher zkw, - String regionName) + String encodedRegionName) throws KeeperException, KeeperException.NoNodeException { - return deleteNode(zkw, regionName, EventType.RS_ZK_REGION_CLOSED); + return deleteNode(zkw, encodedRegionName, EventType.RS_ZK_REGION_CLOSED); } /** @@ -342,8 +343,8 @@ public class ZKAssign { public static boolean deleteClosingNode(ZooKeeperWatcher zkw, HRegionInfo region) throws KeeperException, KeeperException.NoNodeException { - String regionName = region.getEncodedName(); - return deleteNode(zkw, regionName, EventType.M_ZK_REGION_CLOSING); + String encodedRegionName = region.getEncodedName(); + return deleteNode(zkw, encodedRegionName, EventType.M_ZK_REGION_CLOSING); } /** @@ -362,15 +363,15 @@ public class ZKAssign { * of the specified regions transition to being closed/opened. * * @param zkw zk reference - * @param regionName region to be deleted from zk + * @param encodedRegionName region to be deleted from zk * @param expectedState state region must be in for delete to complete * @throws KeeperException if unexpected zookeeper exception * @throws KeeperException.NoNodeException if node does not exist */ - public static boolean deleteNode(ZooKeeperWatcher zkw, String regionName, + public static boolean deleteNode(ZooKeeperWatcher zkw, String encodedRegionName, EventType expectedState) throws KeeperException, KeeperException.NoNodeException { - return deleteNode(zkw, regionName, expectedState, -1); + return deleteNode(zkw, encodedRegionName, expectedState, -1); } /** @@ -389,7 +390,7 @@ public class ZKAssign { * of the specified regions transition to being closed/opened. * * @param zkw zk reference - * @param regionName region to be deleted from zk + * @param encodedRegionName region to be deleted from zk * @param expectedState state region must be in for delete to complete * @param expectedVersion of the znode that is to be deleted. * If expectedVersion need not be compared while deleting the znode @@ -397,12 +398,12 @@ public class ZKAssign { * @throws KeeperException if unexpected zookeeper exception * @throws KeeperException.NoNodeException if node does not exist */ - public static boolean deleteNode(ZooKeeperWatcher zkw, String regionName, + public static boolean deleteNode(ZooKeeperWatcher zkw, String encodedRegionName, EventType expectedState, int expectedVersion) throws KeeperException, KeeperException.NoNodeException { LOG.debug(zkw.prefix("Deleting existing unassigned " + - "node for " + regionName + " that is in expected state " + expectedState)); - String node = getNodeName(zkw, regionName); + "node for " + encodedRegionName + " that is in expected state " + expectedState)); + String node = getNodeName(zkw, encodedRegionName); zkw.sync(node); Stat stat = new Stat(); byte [] bytes = ZKUtil.getDataNoWatch(zkw, node, stat); @@ -413,24 +414,24 @@ public class ZKAssign { RegionTransition rt = getRegionTransition(bytes); EventType et = rt.getEventType(); if (!et.equals(expectedState)) { - LOG.warn(zkw.prefix("Attempting to delete unassigned node " + regionName + " in " + + LOG.warn(zkw.prefix("Attempting to delete unassigned node " + encodedRegionName + " in " + expectedState + " state but node is in " + et + " state")); return false; } if (expectedVersion != -1 && stat.getVersion() != expectedVersion) { - LOG.warn("The node " + regionName + " we are trying to delete is not" + + LOG.warn("The node " + encodedRegionName + " we are trying to delete is not" + " the expected one. Got a version mismatch"); return false; } if(!ZKUtil.deleteNode(zkw, node, stat.getVersion())) { LOG.warn(zkw.prefix("Attempting to delete " + - "unassigned node " + regionName + " in " + expectedState + + "unassigned node " + encodedRegionName + " in " + expectedState + " state but after verifying state, we got a version mismatch")); return false; } LOG.debug(zkw.prefix("Successfully deleted unassigned node for region " + - regionName + " in expected state " + expectedState)); + encodedRegionName + " in expected state " + expectedState)); return true; } @@ -630,6 +631,51 @@ public class ZKAssign { EventType.RS_ZK_REGION_OPENED, expectedVersion); } + /** + * + * @param zkw zk reference + * @param region region to be closed + * @param expectedVersion expected version of the znode + * @return true if the znode exists, has the right version and the right state. False otherwise. + * @throws KeeperException + */ + public static boolean checkClosingState(ZooKeeperWatcher zkw, HRegionInfo region, + int expectedVersion) throws KeeperException { + + final String encoded = getNodeName(zkw, region.getEncodedName()); + zkw.sync(encoded); + + // Read existing data of the node + Stat stat = new Stat(); + byte[] existingBytes = ZKUtil.getDataNoWatch(zkw, encoded, stat); + + if (existingBytes == null) { + LOG.warn(zkw.prefix("Attempt to check the " + + "closing node for " + encoded + + ". The node does not exist")); + return false; + } + + if (expectedVersion != -1 && stat.getVersion() != expectedVersion) { + LOG.warn(zkw.prefix("Attempt to check the " + + "closing node for " + encoded + + ". The node existed but was version " + stat.getVersion() + + " not the expected version " + expectedVersion)); + return false; + } + + RegionTransition rt = getRegionTransition(existingBytes); + + if (!EventType.M_ZK_REGION_CLOSING.equals(rt.getEventType())) { + LOG.warn(zkw.prefix("Attempt to check the " + + "closing node for " + encoded + + ". The node existed but was in an unexpected state: " + rt.getEventType())); + return false; + } + + return true; + } + /** * Method that actually performs unassigned node transitions. * @@ -722,7 +768,8 @@ public class ZKAssign { // Write new data, ensuring data has not changed since we last read it try { - rt = RegionTransition.createRegionTransition(endState, region.getRegionName(), serverName, payload); + rt = RegionTransition.createRegionTransition( + endState, region.getRegionName(), serverName, payload); if(!ZKUtil.setData(zkw, node, rt.toByteArray(), stat.getVersion())) { LOG.warn(zkw.prefix("Attempt to transition the " + "unassigned node for " + encoded + diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/zookeeper/ZKUtil.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/zookeeper/ZKUtil.java index b5837220761..e475dfaeae7 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/zookeeper/ZKUtil.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/zookeeper/ZKUtil.java @@ -1035,7 +1035,13 @@ public class ZKUtil { waitForZKConnectionIfAuthenticating(zkw); zkw.getRecoverableZooKeeper().create(znode, data, createACL(zkw, znode), CreateMode.PERSISTENT); - return zkw.getRecoverableZooKeeper().exists(znode, zkw).getVersion(); + Stat stat = zkw.getRecoverableZooKeeper().exists(znode, zkw); + if (stat == null){ + // Likely a race condition. Someone deleted the znode. + throw KeeperException.create(KeeperException.Code.SYSTEMERROR, + "ZK.exists returned null (i.e.: znode does not exist) for znode=" + znode); + } + return stat.getVersion(); } catch (InterruptedException e) { zkw.interruptedException(e); return -1; diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java index 160c66086fb..e6ae47ddae0 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Random; import java.util.TreeMap; +import java.util.concurrent.ConcurrentSkipListMap; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; @@ -324,7 +325,7 @@ class MockRegionServer implements AdminProtocol, ClientProtocol, RegionServerSer } @Override - public Map getRegionsInTransitionInRS() { + public ConcurrentSkipListMap getRegionsInTransitionInRS() { // TODO Auto-generated method stub return null; } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestRegionServerNoMaster.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestRegionServerNoMaster.java new file mode 100644 index 00000000000..fcc1b9516e2 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestRegionServerNoMaster.java @@ -0,0 +1,356 @@ +/** + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.hbase.regionserver; + +import com.google.protobuf.ServiceException; +import junit.framework.Assert; +import org.apache.hadoop.hbase.HBaseTestingUtility; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.HRegionInfo; +import org.apache.hadoop.hbase.HTableDescriptor; +import org.apache.hadoop.hbase.MediumTests; +import org.apache.hadoop.hbase.NotServingRegionException; +import org.apache.hadoop.hbase.client.HTable; +import org.apache.hadoop.hbase.client.Put; +import org.apache.hadoop.hbase.executor.EventHandler; +import org.apache.hadoop.hbase.protobuf.RequestConverter; +import org.apache.hadoop.hbase.protobuf.generated.AdminProtos; +import org.apache.hadoop.hbase.regionserver.handler.OpenRegionHandler; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.zookeeper.ZKAssign; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; + + +/** + * Tests on the region server, without the master. + */ +@Category(MediumTests.class) +public class TestRegionServerNoMaster { + + private static final int NB_SERVERS = 1; + private static HTable table; + private static final byte[] row = "ee".getBytes(); + + private static HRegionInfo hri; + + private static byte[] regionName; + private static final HBaseTestingUtility HTU = new HBaseTestingUtility(); + + + @BeforeClass + public static void before() throws Exception { + HTU.startMiniCluster(NB_SERVERS); + final byte[] tableName = Bytes.toBytes(TestRegionServerNoMaster.class.getName()); + + // Create table then get the single region for our new table. + table = HTU.createTable(tableName, HConstants.CATALOG_FAMILY); + Put p = new Put(row); + p.add(HConstants.CATALOG_FAMILY, row, row); + table.put(p); + + hri = table.getRegionLocation(row, false).getRegionInfo(); + regionName = hri.getRegionName(); + + // No master + HTU.getHBaseCluster().getMaster().stopMaster(); + } + + @AfterClass + public static void afterClass() throws Exception { + table.close(); + HTU.shutdownMiniCluster(); + } + + @After + public void after() throws Exception { + // Clean the state if the test failed before cleaning the znode + // It does not manage all bad failures, so if there are multiple failures, only + // the first one should be looked at. + ZKAssign.deleteNodeFailSilent(HTU.getZooKeeperWatcher(), hri); + } + + + private static HRegionServer getRS() { + return HTU.getHBaseCluster().getLiveRegionServerThreads().get(0).getRegionServer(); + } + + + /** + * Reopen the region. Reused in multiple tests as we always leave the region open after a test. + */ + private void reopenRegion() throws Exception { + // We reopen. We need a ZK node here, as a open is always triggered by a master. + ZKAssign.createNodeOffline(HTU.getZooKeeperWatcher(), hri, getRS().getServerName()); + // first version is '0' + AdminProtos.OpenRegionRequest orr = RequestConverter.buildOpenRegionRequest(hri, 0); + AdminProtos.OpenRegionResponse responseOpen = getRS().openRegion(null, orr); + Assert.assertTrue(responseOpen.getOpeningStateCount() == 1); + Assert.assertTrue(responseOpen.getOpeningState(0). + equals(AdminProtos.OpenRegionResponse.RegionOpeningState.OPENED)); + + + checkRegionIsOpened(); + } + + private void checkRegionIsOpened() throws Exception { + + while (!getRS().getRegionsInTransitionInRS().isEmpty()) { + Thread.sleep(1); + } + + Assert.assertTrue(getRS().getRegion(regionName).isAvailable()); + + Assert.assertTrue( + ZKAssign.deleteOpenedNode(HTU.getZooKeeperWatcher(), hri.getEncodedName())); + } + + + private void checkRegionIsClosed() throws Exception { + + while (!getRS().getRegionsInTransitionInRS().isEmpty()) { + Thread.sleep(1); + } + + try { + Assert.assertFalse(getRS().getRegion(regionName).isAvailable()); + } catch (NotServingRegionException expected) { + // That's how it work: if the region is closed we have an exception. + } + + // We don't delete the znode here, because there is not always a znode. + } + + + /** + * Close the region without using ZK + */ + private void closeNoZK() throws Exception { + // no transition in ZK + AdminProtos.CloseRegionRequest crr = + RequestConverter.buildCloseRegionRequest(regionName, false); + AdminProtos.CloseRegionResponse responseClose = getRS().closeRegion(null, crr); + Assert.assertTrue(responseClose.getClosed()); + + // now waiting & checking. After a while, the transition should be done and the region closed + checkRegionIsClosed(); + } + + + @Test(timeout = 20000) + public void testCloseByRegionServer() throws Exception { + closeNoZK(); + reopenRegion(); + } + + @Test(timeout = 20000) + public void testCloseByMasterWithoutZNode() throws Exception { + + // Transition in ZK on. This should fail, as there is no znode + AdminProtos.CloseRegionRequest crr = RequestConverter.buildCloseRegionRequest( + regionName, true); + AdminProtos.CloseRegionResponse responseClose = getRS().closeRegion(null, crr); + Assert.assertTrue(responseClose.getClosed()); + + // now waiting. After a while, the transition should be done + while (!getRS().getRegionsInTransitionInRS().isEmpty()) { + Thread.sleep(1); + } + + // the region is still available, the close got rejected at the end + Assert.assertTrue("The close should have failed", getRS().getRegion(regionName).isAvailable()); + } + + @Test(timeout = 20000) + public void testOpenCloseByMasterWithZNode() throws Exception { + + ZKAssign.createNodeClosing(HTU.getZooKeeperWatcher(), hri, getRS().getServerName()); + + AdminProtos.CloseRegionRequest crr = RequestConverter.buildCloseRegionRequest( + regionName, true); + AdminProtos.CloseRegionResponse responseClose = getRS().closeRegion(null, crr); + Assert.assertTrue(responseClose.getClosed()); + + checkRegionIsClosed(); + + ZKAssign.deleteClosedNode(HTU.getZooKeeperWatcher(), hri.getEncodedName()); + + reopenRegion(); + } + + /** + * Test that we can send multiple openRegion to the region server. + * This is used when: + * - there is a SocketTimeout: in this case, the master does not know if the region server + * received the request before the timeout. + * - We have a socket error during the operation: same stuff: we don't know + * - a master failover: if we find a znode in thz M_ZK_REGION_OFFLINE, we don't know if + * the region server has received the query or not. Only solution to be efficient: re-ask + * immediately. + */ + @Test(timeout = 20000) + public void testMultipleOpen() throws Exception { + + // We close + closeNoZK(); + checkRegionIsClosed(); + + // We reopen. We need a ZK node here, as a open is always triggered by a master. + ZKAssign.createNodeOffline(HTU.getZooKeeperWatcher(), hri, getRS().getServerName()); + + // We're sending multiple requests in a row. The region server must handle this nicely. + for (int i = 0; i < 10; i++) { + AdminProtos.OpenRegionRequest orr = RequestConverter.buildOpenRegionRequest(hri, 0); + AdminProtos.OpenRegionResponse responseOpen = getRS().openRegion(null, orr); + Assert.assertTrue(responseOpen.getOpeningStateCount() == 1); + + AdminProtos.OpenRegionResponse.RegionOpeningState ors = responseOpen.getOpeningState(0); + Assert.assertTrue("request " + i + " failed", + ors.equals(AdminProtos.OpenRegionResponse.RegionOpeningState.OPENED) || + ors.equals(AdminProtos.OpenRegionResponse.RegionOpeningState.ALREADY_OPENED) + ); + } + + checkRegionIsOpened(); + } + + @Test(timeout = 20000) + public void testMultipleCloseFromMaster() throws Exception { + + // As opening, we must support multiple requests on the same region + ZKAssign.createNodeClosing(HTU.getZooKeeperWatcher(), hri, getRS().getServerName()); + for (int i = 0; i < 10; i++) { + AdminProtos.CloseRegionRequest crr = + RequestConverter.buildCloseRegionRequest(regionName, 0, null, true); + try { + AdminProtos.CloseRegionResponse responseClose = getRS().closeRegion(null, crr); + Assert.assertEquals("The first request should succeeds", 0, i); + Assert.assertTrue("request " + i + " failed", + responseClose.getClosed() || responseClose.hasClosed()); + } catch (ServiceException se) { + Assert.assertTrue("The next queries should throw an exception.", i > 0); + } + } + + checkRegionIsClosed(); + + Assert.assertTrue( + ZKAssign.deleteClosedNode(HTU.getZooKeeperWatcher(), hri.getEncodedName()) + ); + + reopenRegion(); + } + + /** + * Test that if we do a close while opening it stops the opening. + */ + @Test(timeout = 20000) + public void testCancelOpeningWithoutZK() throws Exception { + // We close + closeNoZK(); + checkRegionIsClosed(); + + // Let do the initial steps, without having a handler + ZKAssign.createNodeOffline(HTU.getZooKeeperWatcher(), hri, getRS().getServerName()); + getRS().getRegionsInTransitionInRS().put(hri.getEncodedNameAsBytes(), Boolean.TRUE); + + // That's a close without ZK. + AdminProtos.CloseRegionRequest crr = + RequestConverter.buildCloseRegionRequest(regionName, false); + try { + getRS().closeRegion(null, crr); + Assert.assertTrue(false); + } catch (ServiceException expected) { + } + + // The state in RIT should have changed to close + Assert.assertEquals(Boolean.FALSE, getRS().getRegionsInTransitionInRS().get( + hri.getEncodedNameAsBytes())); + + // Let's start the open handler + HTableDescriptor htd = getRS().tableDescriptors.get(hri.getTableName()); + getRS().service.submit(new OpenRegionHandler(getRS(), getRS(), hri, htd, 0)); + + // The open handler should have removed the region from RIT but kept the region closed + checkRegionIsClosed(); + + // The open handler should have updated the value in ZK. + Assert.assertTrue(ZKAssign.deleteNode( + getRS().getZooKeeperWatcher(), hri.getEncodedName(), + EventHandler.EventType.RS_ZK_REGION_FAILED_OPEN, 1) + ); + + reopenRegion(); + } + + /** + * Test an open then a close with ZK. This is going to mess-up the ZK states, so + * the opening will fail as well because it doesn't find what it expects in ZK. + */ + @Test(timeout = 20000) + public void testCancelOpeningWithZK() throws Exception { + // We close + closeNoZK(); + checkRegionIsClosed(); + + // Let do the initial steps, without having a handler + getRS().getRegionsInTransitionInRS().put(hri.getEncodedNameAsBytes(), Boolean.TRUE); + + // That's a close without ZK. + ZKAssign.createNodeClosing(HTU.getZooKeeperWatcher(), hri, getRS().getServerName()); + AdminProtos.CloseRegionRequest crr = + RequestConverter.buildCloseRegionRequest(regionName, false); + try { + getRS().closeRegion(null, crr); + Assert.assertTrue(false); + } catch (ServiceException expected) { + Assert.assertTrue(expected.getCause() instanceof NotServingRegionException); + } + + // The close should have left the ZK state as it is: it's the job the AM to delete it + Assert.assertTrue(ZKAssign.deleteNode( + getRS().getZooKeeperWatcher(), hri.getEncodedName(), + EventHandler.EventType.M_ZK_REGION_CLOSING, 0) + ); + + // The state in RIT should have changed to close + Assert.assertEquals(Boolean.FALSE, getRS().getRegionsInTransitionInRS().get( + hri.getEncodedNameAsBytes())); + + // Let's start the open handler + // It should not succeed for two reasons: + // 1) There is no ZK node + // 2) The region in RIT was changed. + // The order is more or less implementation dependant. + HTableDescriptor htd = getRS().tableDescriptors.get(hri.getTableName()); + getRS().service.submit(new OpenRegionHandler(getRS(), getRS(), hri, htd, 0)); + + // The open handler should have removed the region from RIT but kept the region closed + checkRegionIsClosed(); + + // We should not find any znode here. + Assert.assertEquals(-1, ZKAssign.getVersion(HTU.getZooKeeperWatcher(), hri)); + + reopenRegion(); + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/handler/TestOpenRegionHandler.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/handler/TestOpenRegionHandler.java index aa18cb723a0..32b4b6e9ccd 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/handler/TestOpenRegionHandler.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/handler/TestOpenRegionHandler.java @@ -88,7 +88,7 @@ public class TestOpenRegionHandler { @Test public void testYankingRegionFromUnderIt() throws IOException, NodeExistsException, KeeperException { final Server server = new MockServer(HTU); - final RegionServerServices rss = new MockRegionServerServices(); + final RegionServerServices rss = new MockRegionServerServices(HTU.getZooKeeperWatcher()); HTableDescriptor htd = TEST_HTD; final HRegionInfo hri = TEST_HRI; diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/util/MockRegionServerServices.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/util/MockRegionServerServices.java index 5eb85475ba2..25569c5ee23 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/util/MockRegionServerServices.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/util/MockRegionServerServices.java @@ -36,7 +36,6 @@ import org.apache.hadoop.hbase.regionserver.Leases; import org.apache.hadoop.hbase.regionserver.RegionServerAccounting; import org.apache.hadoop.hbase.regionserver.RegionServerServices; import org.apache.hadoop.hbase.regionserver.wal.HLog; -import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; import org.apache.zookeeper.KeeperException; @@ -49,6 +48,15 @@ public class MockRegionServerServices implements RegionServerServices { private final ConcurrentSkipListMap rit = new ConcurrentSkipListMap(Bytes.BYTES_COMPARATOR); private HFileSystem hfs = null; + private ZooKeeperWatcher zkw = null; + + public MockRegionServerServices(ZooKeeperWatcher zkw){ + this.zkw = zkw; + } + + public MockRegionServerServices(){ + this(null); + } @Override public boolean removeFromOnlineRegions(String encodedRegionName, ServerName destination) { @@ -112,7 +120,7 @@ public class MockRegionServerServices implements RegionServerServices { @Override public ZooKeeperWatcher getZooKeeper() { - return null; + return zkw; } public RegionServerAccounting getRegionServerAccounting() {