Java tutorial
/** * Copyright The Apache Software Foundation * * 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 limitationsME * under the License. */ package org.apache.hadoop.hbase.regionserver; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.hbase.classification.InterfaceAudience; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.HRegionInfo; import org.apache.hadoop.hbase.MetaMutationAnnotation; import org.apache.hadoop.hbase.MetaTableAccessor; import org.apache.hadoop.hbase.Server; import org.apache.hadoop.hbase.ServerName; import org.apache.hadoop.hbase.client.Delete; import org.apache.hadoop.hbase.client.Mutation; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.protobuf.generated.RegionServerStatusProtos.RegionStateTransition.TransitionCode; import org.apache.hadoop.hbase.regionserver.SplitTransactionImpl.LoggingProgressable; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.hadoop.hbase.util.Pair; import com.google.common.annotations.VisibleForTesting; @InterfaceAudience.Private public class RegionMergeTransactionImpl implements RegionMergeTransaction { private static final Log LOG = LogFactory.getLog(RegionMergeTransactionImpl.class); // Merged region info private HRegionInfo mergedRegionInfo; // region_a sorts before region_b private final HRegion region_a; private final HRegion region_b; // merges dir is under region_a private final Path mergesdir; // We only merge adjacent regions if forcible is false private final boolean forcible; private final long masterSystemTime; /* * Transaction state for listener, only valid during execute and * rollback */ private RegionMergeTransactionPhase currentPhase = RegionMergeTransactionPhase.STARTED; private Server server; private RegionServerServices rsServices; public static class JournalEntryImpl implements JournalEntry { private RegionMergeTransactionPhase type; private long timestamp; public JournalEntryImpl(RegionMergeTransactionPhase type) { this(type, EnvironmentEdgeManager.currentTime()); } public JournalEntryImpl(RegionMergeTransactionPhase type, long timestamp) { this.type = type; this.timestamp = timestamp; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(type); sb.append(" at "); sb.append(timestamp); return sb.toString(); } @Override public RegionMergeTransactionPhase getPhase() { return type; } @Override public long getTimeStamp() { return timestamp; } } /* * Journal of how far the merge transaction has progressed. */ private final List<JournalEntry> journal = new ArrayList<JournalEntry>(); /** * Listeners */ private final ArrayList<TransactionListener> listeners = new ArrayList<TransactionListener>(); private static IOException closedByOtherException = new IOException( "Failed to close region: already closed by another thread"); private RegionServerCoprocessorHost rsCoprocessorHost = null; /** * Constructor * @param a region a to merge * @param b region b to merge * @param forcible if false, we will only merge adjacent regions */ public RegionMergeTransactionImpl(final Region a, final Region b, final boolean forcible) { this(a, b, forcible, EnvironmentEdgeManager.currentTime()); } /** * Constructor * @param a region a to merge * @param b region b to merge * @param forcible if false, we will only merge adjacent regions * @param masterSystemTime the time at the master side */ public RegionMergeTransactionImpl(final Region a, final Region b, final boolean forcible, long masterSystemTime) { if (a.getRegionInfo().compareTo(b.getRegionInfo()) <= 0) { this.region_a = (HRegion) a; this.region_b = (HRegion) b; } else { this.region_a = (HRegion) b; this.region_b = (HRegion) a; } this.forcible = forcible; this.masterSystemTime = masterSystemTime; this.mergesdir = region_a.getRegionFileSystem().getMergesDir(); } private void transition(RegionMergeTransactionPhase nextPhase) throws IOException { transition(nextPhase, false); } private void transition(RegionMergeTransactionPhase nextPhase, boolean isRollback) throws IOException { if (!isRollback) { // Add to the journal first, because if the listener throws an exception // we need to roll back starting at 'nextPhase' this.journal.add(new JournalEntryImpl(nextPhase)); } for (int i = 0; i < listeners.size(); i++) { TransactionListener listener = listeners.get(i); if (!isRollback) { listener.transition(this, currentPhase, nextPhase); } else { listener.rollback(this, currentPhase, nextPhase); } } currentPhase = nextPhase; } @Override public boolean prepare(final RegionServerServices services) throws IOException { if (!region_a.getTableDesc().getTableName().equals(region_b.getTableDesc().getTableName())) { LOG.info("Can't merge regions " + region_a + "," + region_b + " because they do not belong to the same table"); return false; } if (region_a.getRegionInfo().equals(region_b.getRegionInfo())) { LOG.info("Can't merge the same region " + region_a); return false; } if (!forcible && !HRegionInfo.areAdjacent(region_a.getRegionInfo(), region_b.getRegionInfo())) { String msg = "Skip merging " + region_a.getRegionInfo().getRegionNameAsString() + " and " + region_b.getRegionInfo().getRegionNameAsString() + ", because they are not adjacent."; LOG.info(msg); return false; } if (!this.region_a.isMergeable() || !this.region_b.isMergeable()) { return false; } try { boolean regionAHasMergeQualifier = hasMergeQualifierInMeta(services, region_a.getRegionInfo().getRegionName()); if (regionAHasMergeQualifier || hasMergeQualifierInMeta(services, region_b.getRegionInfo().getRegionName())) { LOG.debug("Region " + (regionAHasMergeQualifier ? region_a.getRegionInfo().getRegionNameAsString() : region_b.getRegionInfo().getRegionNameAsString()) + " is not mergeable because it has merge qualifier in META"); return false; } } catch (IOException e) { LOG.warn("Failed judging whether merge transaction is available for " + region_a.getRegionInfo().getRegionNameAsString() + " and " + region_b.getRegionInfo().getRegionNameAsString(), e); return false; } // WARN: make sure there is no parent region of the two merging regions in // hbase:meta If exists, fixing up daughters would cause daughter regions(we // have merged one) online again when we restart master, so we should clear // the parent region to prevent the above case // Since HBASE-7721, we don't need fix up daughters any more. so here do // nothing this.mergedRegionInfo = getMergedRegionInfo(region_a.getRegionInfo(), region_b.getRegionInfo()); transition(RegionMergeTransactionPhase.PREPARED); return true; } @Override public Region execute(final Server server, final RegionServerServices services) throws IOException { this.server = server; this.rsServices = services; if (rsCoprocessorHost == null) { rsCoprocessorHost = server != null ? ((HRegionServer) server).getRegionServerCoprocessorHost() : null; } HRegion mergedRegion = createMergedRegion(server, services); if (rsCoprocessorHost != null) { rsCoprocessorHost.postMergeCommit(this.region_a, this.region_b, mergedRegion); } stepsAfterPONR(server, services, mergedRegion); transition(RegionMergeTransactionPhase.COMPLETED); return mergedRegion; } @VisibleForTesting public void stepsAfterPONR(final Server server, final RegionServerServices services, HRegion mergedRegion) throws IOException { openMergedRegion(server, services, mergedRegion); if (rsCoprocessorHost != null) { rsCoprocessorHost.postMerge(this.region_a, this.region_b, mergedRegion); } } /** * Prepare the merged region and region files. * @param server Hosting server instance. Can be null when testing * @param services Used to online/offline regions. * @return merged region * @throws IOException If thrown, transaction failed. Call * {@link #rollback(Server, RegionServerServices)} */ private HRegion createMergedRegion(final Server server, final RegionServerServices services) throws IOException { LOG.info("Starting merge of " + region_a + " and " + region_b.getRegionInfo().getRegionNameAsString() + ", forcible=" + forcible); if ((server != null && server.isStopped()) || (services != null && services.isStopping())) { throw new IOException("Server is stopped or stopping"); } if (rsCoprocessorHost != null) { if (rsCoprocessorHost.preMerge(this.region_a, this.region_b)) { throw new IOException( "Coprocessor bypassing regions " + this.region_a + " " + this.region_b + " merge."); } } // If true, no cluster to write meta edits to or to use coordination. boolean testing = server == null ? true : server.getConfiguration().getBoolean("hbase.testing.nocluster", false); HRegion mergedRegion = stepsBeforePONR(server, services, testing); @MetaMutationAnnotation List<Mutation> metaEntries = new ArrayList<Mutation>(); if (rsCoprocessorHost != null) { if (rsCoprocessorHost.preMergeCommit(this.region_a, this.region_b, metaEntries)) { throw new IOException( "Coprocessor bypassing regions " + this.region_a + " " + this.region_b + " merge."); } try { for (Mutation p : metaEntries) { HRegionInfo.parseRegionName(p.getRow()); } } catch (IOException e) { LOG.error("Row key of mutation from coprocessor is not parsable as region name." + "Mutations from coprocessor should only be for hbase:meta table.", e); throw e; } } // This is the point of no return. Similar with SplitTransaction. // IF we reach the PONR then subsequent failures need to crash out this // regionserver transition(RegionMergeTransactionPhase.PONR); // Add merged region and delete region_a and region_b // as an atomic update. See HBASE-7721. This update to hbase:meta makes the region // will determine whether the region is merged or not in case of failures. // If it is successful, master will roll-forward, if not, master will // rollback if (services != null && !services.reportRegionStateTransition(TransitionCode.MERGE_PONR, mergedRegionInfo, region_a.getRegionInfo(), region_b.getRegionInfo())) { // Passed PONR, let SSH clean it up throw new IOException("Failed to notify master that merge passed PONR: " + region_a.getRegionInfo().getRegionNameAsString() + " and " + region_b.getRegionInfo().getRegionNameAsString()); } return mergedRegion; } @VisibleForTesting public void prepareMutationsForMerge(HRegionInfo mergedRegion, HRegionInfo regionA, HRegionInfo regionB, ServerName serverName, List<Mutation> mutations) throws IOException { HRegionInfo copyOfMerged = new HRegionInfo(mergedRegion); // use the maximum of what master passed us vs local time. long time = Math.max(EnvironmentEdgeManager.currentTime(), masterSystemTime); // Put for parent Put putOfMerged = MetaTableAccessor.makePutFromRegionInfo(copyOfMerged, time); putOfMerged.add(HConstants.CATALOG_FAMILY, HConstants.MERGEA_QUALIFIER, regionA.toByteArray()); putOfMerged.add(HConstants.CATALOG_FAMILY, HConstants.MERGEB_QUALIFIER, regionB.toByteArray()); mutations.add(putOfMerged); // Deletes for merging regions Delete deleteA = MetaTableAccessor.makeDeleteFromRegionInfo(regionA, time); Delete deleteB = MetaTableAccessor.makeDeleteFromRegionInfo(regionB, time); mutations.add(deleteA); mutations.add(deleteB); // The merged is a new region, openSeqNum = 1 is fine. addLocation(putOfMerged, serverName, 1); } @VisibleForTesting Put addLocation(final Put p, final ServerName sn, long openSeqNum) { p.add(HConstants.CATALOG_FAMILY, HConstants.SERVER_QUALIFIER, Bytes.toBytes(sn.getHostAndPort())); p.add(HConstants.CATALOG_FAMILY, HConstants.STARTCODE_QUALIFIER, Bytes.toBytes(sn.getStartcode())); p.add(HConstants.CATALOG_FAMILY, HConstants.SEQNUM_QUALIFIER, Bytes.toBytes(openSeqNum)); return p; } @VisibleForTesting public HRegion stepsBeforePONR(final Server server, final RegionServerServices services, boolean testing) throws IOException { if (services != null && !services.reportRegionStateTransition(TransitionCode.READY_TO_MERGE, mergedRegionInfo, region_a.getRegionInfo(), region_b.getRegionInfo())) { throw new IOException( "Failed to get ok from master to merge " + region_a.getRegionInfo().getRegionNameAsString() + " and " + region_b.getRegionInfo().getRegionNameAsString()); } transition(RegionMergeTransactionPhase.SET_MERGING); this.region_a.getRegionFileSystem().createMergesDir(); transition(RegionMergeTransactionPhase.CREATED_MERGE_DIR); Map<byte[], List<StoreFile>> hstoreFilesOfRegionA = closeAndOfflineRegion(services, this.region_a, true, testing); Map<byte[], List<StoreFile>> hstoreFilesOfRegionB = closeAndOfflineRegion(services, this.region_b, false, testing); assert hstoreFilesOfRegionA != null && hstoreFilesOfRegionB != null; // mergeStoreFiles creates merged region dirs under the region_a merges dir // Nothing to unroll here if failure -- clean up of CREATE_MERGE_DIR will // clean this up. mergeStoreFiles(hstoreFilesOfRegionA, hstoreFilesOfRegionB); // Log to the journal that we are creating merged region. We could fail // halfway through. If we do, we could have left // stuff in fs that needs cleanup -- a storefile or two. Thats why we // add entry to journal BEFORE rather than AFTER the change. transition(RegionMergeTransactionPhase.STARTED_MERGED_REGION_CREATION); HRegion mergedRegion = createMergedRegionFromMerges(this.region_a, this.region_b, this.mergedRegionInfo); return mergedRegion; } /** * Create a merged region from the merges directory under region a. In order * to mock it for tests, place it with a new method. * @param a hri of region a * @param b hri of region b * @param mergedRegion hri of merged region * @return merged HRegion. * @throws IOException */ @VisibleForTesting HRegion createMergedRegionFromMerges(final HRegion a, final HRegion b, final HRegionInfo mergedRegion) throws IOException { return a.createMergedRegionFromMerges(mergedRegion, b); } /** * Close the merging region and offline it in regionserver * @param services * @param region * @param isRegionA true if it is merging region a, false if it is region b * @param testing true if it is testing * @return a map of family name to list of store files * @throws IOException */ private Map<byte[], List<StoreFile>> closeAndOfflineRegion(final RegionServerServices services, final HRegion region, final boolean isRegionA, final boolean testing) throws IOException { Map<byte[], List<StoreFile>> hstoreFilesToMerge = null; Exception exceptionToThrow = null; try { hstoreFilesToMerge = region.close(false); } catch (Exception e) { exceptionToThrow = e; } if (exceptionToThrow == null && hstoreFilesToMerge == null) { // The region was closed by a concurrent thread. We can't continue // with the merge, instead we must just abandon the merge. If we // reopen or merge this could cause problems because the region has // probably already been moved to a different server, or is in the // process of moving to a different server. exceptionToThrow = closedByOtherException; } if (exceptionToThrow != closedByOtherException) { transition(isRegionA ? RegionMergeTransactionPhase.CLOSED_REGION_A : RegionMergeTransactionPhase.CLOSED_REGION_B); } if (exceptionToThrow != null) { if (exceptionToThrow instanceof IOException) throw (IOException) exceptionToThrow; throw new IOException(exceptionToThrow); } if (!testing) { services.removeFromOnlineRegions(region, null); } transition(isRegionA ? RegionMergeTransactionPhase.OFFLINED_REGION_A : RegionMergeTransactionPhase.OFFLINED_REGION_B); return hstoreFilesToMerge; } /** * Get merged region info through the specified two regions * @param a merging region A * @param b merging region B * @return the merged region info */ @VisibleForTesting static HRegionInfo getMergedRegionInfo(final HRegionInfo a, final HRegionInfo b) { long rid = EnvironmentEdgeManager.currentTime(); // Regionid is timestamp. Merged region's id can't be less than that of // merging regions else will insert at wrong location in hbase:meta if (rid < a.getRegionId() || rid < b.getRegionId()) { LOG.warn("Clock skew; merging regions id are " + a.getRegionId() + " and " + b.getRegionId() + ", but current time here is " + rid); rid = Math.max(a.getRegionId(), b.getRegionId()) + 1; } byte[] startKey = null; byte[] endKey = null; // Choose the smaller as start key if (a.compareTo(b) <= 0) { startKey = a.getStartKey(); } else { startKey = b.getStartKey(); } // Choose the bigger as end key if (Bytes.equals(a.getEndKey(), HConstants.EMPTY_BYTE_ARRAY) || (!Bytes.equals(b.getEndKey(), HConstants.EMPTY_BYTE_ARRAY) && Bytes.compareTo(a.getEndKey(), b.getEndKey()) > 0)) { endKey = a.getEndKey(); } else { endKey = b.getEndKey(); } // Merged region is sorted between two merging regions in META HRegionInfo mergedRegionInfo = new HRegionInfo(a.getTable(), startKey, endKey, false, rid); return mergedRegionInfo; } /** * Perform time consuming opening of the merged region. * @param server Hosting server instance. Can be null when testing * @param services Used to online/offline regions. * @param merged the merged region * @throws IOException If thrown, transaction failed. Call * {@link #rollback(Server, RegionServerServices)} */ @VisibleForTesting void openMergedRegion(final Server server, final RegionServerServices services, HRegion merged) throws IOException { boolean stopped = server != null && server.isStopped(); boolean stopping = services != null && services.isStopping(); if (stopped || stopping) { LOG.info("Not opening merged region " + merged.getRegionInfo().getRegionNameAsString() + " because stopping=" + stopping + ", stopped=" + stopped); return; } HRegionInfo hri = merged.getRegionInfo(); LoggingProgressable reporter = server == null ? null : new LoggingProgressable(hri, server.getConfiguration() .getLong("hbase.regionserver.regionmerge.open.log.interval", 10000)); merged.openHRegion(reporter); if (services != null) { if (!services.reportRegionStateTransition(TransitionCode.MERGED, mergedRegionInfo, region_a.getRegionInfo(), region_b.getRegionInfo())) { throw new IOException( "Failed to report merged region to master: " + mergedRegionInfo.getShortNameToLog()); } services.addToOnlineRegions(merged); } } /** * Create reference file(s) of merging regions under the region_a merges dir * @param hstoreFilesOfRegionA * @param hstoreFilesOfRegionB * @throws IOException */ private void mergeStoreFiles(Map<byte[], List<StoreFile>> hstoreFilesOfRegionA, Map<byte[], List<StoreFile>> hstoreFilesOfRegionB) throws IOException { // Create reference file(s) of region A in mergdir HRegionFileSystem fs_a = this.region_a.getRegionFileSystem(); for (Map.Entry<byte[], List<StoreFile>> entry : hstoreFilesOfRegionA.entrySet()) { String familyName = Bytes.toString(entry.getKey()); for (StoreFile storeFile : entry.getValue()) { fs_a.mergeStoreFile(this.mergedRegionInfo, familyName, storeFile, this.mergesdir); } } // Create reference file(s) of region B in mergedir HRegionFileSystem fs_b = this.region_b.getRegionFileSystem(); for (Map.Entry<byte[], List<StoreFile>> entry : hstoreFilesOfRegionB.entrySet()) { String familyName = Bytes.toString(entry.getKey()); for (StoreFile storeFile : entry.getValue()) { fs_b.mergeStoreFile(this.mergedRegionInfo, familyName, storeFile, this.mergesdir); } } } @Override public boolean rollback(final Server server, final RegionServerServices services) throws IOException { assert this.mergedRegionInfo != null; this.server = server; this.rsServices = services; // Coprocessor callback if (rsCoprocessorHost != null) { rsCoprocessorHost.preRollBackMerge(this.region_a, this.region_b); } boolean result = true; ListIterator<JournalEntry> iterator = this.journal.listIterator(this.journal.size()); // Iterate in reverse. while (iterator.hasPrevious()) { JournalEntry je = iterator.previous(); transition(je.getPhase(), true); switch (je.getPhase()) { case SET_MERGING: if (services != null && !services.reportRegionStateTransition(TransitionCode.MERGE_REVERTED, mergedRegionInfo, region_a.getRegionInfo(), region_b.getRegionInfo())) { return false; } break; case CREATED_MERGE_DIR: this.region_a.writestate.writesEnabled = true; this.region_b.writestate.writesEnabled = true; this.region_a.getRegionFileSystem().cleanupMergesDir(); break; case CLOSED_REGION_A: try { // So, this returns a seqid but if we just closed and then reopened, // we should be ok. On close, we flushed using sequenceid obtained // from hosting regionserver so no need to propagate the sequenceid // returned out of initialize below up into regionserver as we // normally do. this.region_a.initialize(); } catch (IOException e) { LOG.error("Failed rollbacking CLOSED_REGION_A of region " + region_a.getRegionInfo().getRegionNameAsString(), e); throw new RuntimeException(e); } break; case OFFLINED_REGION_A: if (services != null) services.addToOnlineRegions(this.region_a); break; case CLOSED_REGION_B: try { this.region_b.initialize(); } catch (IOException e) { LOG.error("Failed rollbacking CLOSED_REGION_A of region " + region_b.getRegionInfo().getRegionNameAsString(), e); throw new RuntimeException(e); } break; case OFFLINED_REGION_B: if (services != null) services.addToOnlineRegions(this.region_b); break; case STARTED_MERGED_REGION_CREATION: this.region_a.getRegionFileSystem().cleanupMergedRegion(this.mergedRegionInfo); break; case PONR: // We got to the point-of-no-return so we need to just abort. Return // immediately. Do not clean up created merged regions. return false; // Informational states only case STARTED: case PREPARED: case COMPLETED: break; default: throw new RuntimeException("Unhandled journal entry: " + je); } } // Coprocessor callback if (rsCoprocessorHost != null) { rsCoprocessorHost.postRollBackMerge(this.region_a, this.region_b); } return result; } @Override public HRegionInfo getMergedRegionInfo() { return this.mergedRegionInfo; } @VisibleForTesting Path getMergesDir() { return this.mergesdir; } /** * Checks if the given region has merge qualifier in hbase:meta * @param services * @param regionName name of specified region * @return true if the given region has merge qualifier in META.(It will be * cleaned by CatalogJanitor) * @throws IOException */ @VisibleForTesting boolean hasMergeQualifierInMeta(final RegionServerServices services, final byte[] regionName) throws IOException { if (services == null) return false; // Get merge regions if it is a merged region and already has merge // qualifier Pair<HRegionInfo, HRegionInfo> mergeRegions = MetaTableAccessor .getRegionsFromMergeQualifier(services.getConnection(), regionName); if (mergeRegions != null && (mergeRegions.getFirst() != null || mergeRegions.getSecond() != null)) { // It has merge qualifier return true; } return false; } @Override public List<JournalEntry> getJournal() { return journal; } @Override public RegionMergeTransaction registerTransactionListener(TransactionListener listener) { listeners.add(listener); return this; } @Override public Server getServer() { return server; } @Override public RegionServerServices getRegionServerServices() { return rsServices; } }