View Javadoc

1   /**
2    * Copyright The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements. See the NOTICE file distributed with this
6    * work for additional information regarding copyright ownership. The ASF
7    * licenses this file to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   * http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16   * License for the specific language governing permissions and limitationsME
17   * under the License.
18   */
19  package org.apache.hadoop.hbase.regionserver;
20  
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.ListIterator;
25  import java.util.Map;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.apache.hadoop.hbase.classification.InterfaceAudience;
30  import org.apache.hadoop.fs.Path;
31  import org.apache.hadoop.hbase.HConstants;
32  import org.apache.hadoop.hbase.HRegionInfo;
33  import org.apache.hadoop.hbase.MetaMutationAnnotation;
34  import org.apache.hadoop.hbase.Server;
35  import org.apache.hadoop.hbase.ServerName;
36  import org.apache.hadoop.hbase.MetaTableAccessor;
37  import org.apache.hadoop.hbase.client.Delete;
38  import org.apache.hadoop.hbase.client.HConnection;
39  import org.apache.hadoop.hbase.client.Mutation;
40  import org.apache.hadoop.hbase.client.Put;
41  import org.apache.hadoop.hbase.coordination.BaseCoordinatedStateManager;
42  import org.apache.hadoop.hbase.coordination.RegionMergeCoordination.RegionMergeDetails;
43  import org.apache.hadoop.hbase.protobuf.generated.RegionServerStatusProtos.RegionStateTransition.TransitionCode;
44  import org.apache.hadoop.hbase.regionserver.SplitTransaction.JournalEntry;
45  import org.apache.hadoop.hbase.regionserver.SplitTransactionImpl.LoggingProgressable;
46  import org.apache.hadoop.hbase.security.User;
47  import org.apache.hadoop.hbase.util.Bytes;
48  import org.apache.hadoop.hbase.util.ConfigUtil;
49  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
50  import org.apache.hadoop.hbase.util.Pair;
51  import org.apache.zookeeper.KeeperException;
52  
53  @InterfaceAudience.Private
54  public class RegionMergeTransactionImpl implements RegionMergeTransaction {
55    private static final Log LOG = LogFactory.getLog(RegionMergeTransactionImpl.class);
56  
57    // Merged region info
58    private HRegionInfo mergedRegionInfo;
59    // region_a sorts before region_b
60    private final HRegion region_a;
61    private final HRegion region_b;
62    // merges dir is under region_a
63    private final Path mergesdir;
64    // We only merge adjacent regions if forcible is false
65    private final boolean forcible;
66    private boolean useCoordinationForAssignment;
67    private final long masterSystemTime;
68  
69    /*
70     * Transaction state for listener, only valid during execute and
71     * rollback
72     */
73    private RegionMergeTransactionPhase currentPhase = RegionMergeTransactionPhase.STARTED;
74    private Server server;
75    private RegionServerServices rsServices;
76  
77    public static class JournalEntryImpl implements JournalEntry {
78      private RegionMergeTransactionPhase type;
79      private long timestamp;
80  
81      public JournalEntryImpl(RegionMergeTransactionPhase type) {
82        this(type, EnvironmentEdgeManager.currentTime());
83      }
84  
85      public JournalEntryImpl(RegionMergeTransactionPhase type, long timestamp) {
86        this.type = type;
87        this.timestamp = timestamp;
88      }
89  
90      @Override
91      public String toString() {
92        StringBuilder sb = new StringBuilder();
93        sb.append(type);
94        sb.append(" at ");
95        sb.append(timestamp);
96        return sb.toString();
97      }
98  
99      @Override
100     public RegionMergeTransactionPhase getPhase() {
101       return type;
102     }
103 
104     @Override
105     public long getTimeStamp() {
106       return timestamp;
107     }
108   }
109 
110   /*
111    * Journal of how far the merge transaction has progressed.
112    */
113   private final List<JournalEntry> journal = new ArrayList<JournalEntry>();
114 
115   /*
116    * Listeners
117    */
118   private final ArrayList<TransactionListener> listeners = new ArrayList<TransactionListener>();
119 
120   private static IOException closedByOtherException = new IOException(
121       "Failed to close region: already closed by another thread");
122 
123   private RegionServerCoprocessorHost rsCoprocessorHost = null;
124 
125   private RegionMergeDetails rmd;
126 
127   /**
128    * Constructor
129    * @param a region a to merge
130    * @param b region b to merge
131    * @param forcible if false, we will only merge adjacent regions
132    */
133   public RegionMergeTransactionImpl(final Region a, final Region b,
134       final boolean forcible) {
135     this(a, b, forcible, EnvironmentEdgeManager.currentTime());
136   }
137   /**
138    * Constructor
139    * @param a region a to merge
140    * @param b region b to merge
141    * @param forcible if false, we will only merge adjacent regions
142    * @param masterSystemTime the time at the master side
143    */
144   public RegionMergeTransactionImpl(final Region a, final Region b,
145       final boolean forcible, long masterSystemTime) {
146     if (a.getRegionInfo().compareTo(b.getRegionInfo()) <= 0) {
147       this.region_a = (HRegion)a;
148       this.region_b = (HRegion)b;
149     } else {
150       this.region_a = (HRegion)b;
151       this.region_b = (HRegion)a;
152     }
153     this.forcible = forcible;
154     this.masterSystemTime = masterSystemTime;
155     this.mergesdir = region_a.getRegionFileSystem().getMergesDir();
156   }
157 
158   private void transition(RegionMergeTransactionPhase nextPhase) throws IOException {
159     transition(nextPhase, false);
160   }
161 
162   private void transition(RegionMergeTransactionPhase nextPhase, boolean isRollback)
163       throws IOException {
164     if (!isRollback) {
165       // Add to the journal first, because if the listener throws an exception
166       // we need to roll back starting at 'nextPhase'
167       this.journal.add(new JournalEntryImpl(nextPhase));
168     }
169     for (int i = 0; i < listeners.size(); i++) {
170       TransactionListener listener = listeners.get(i);
171       if (!isRollback) {
172         listener.transition(this, currentPhase, nextPhase);
173       } else {
174         listener.rollback(this, currentPhase, nextPhase);
175       }
176     }
177     currentPhase = nextPhase;
178   }
179 
180   /**
181    * Does checks on merge inputs.
182    * @param services
183    * @return <code>true</code> if the regions are mergeable else
184    *         <code>false</code> if they are not (e.g. its already closed, etc.).
185    */
186   @Override
187   public boolean prepare(final RegionServerServices services) throws IOException {
188     if (!region_a.getTableDesc().getTableName()
189         .equals(region_b.getTableDesc().getTableName())) {
190       LOG.info("Can't merge regions " + region_a + "," + region_b
191           + " because they do not belong to the same table");
192       return false;
193     }
194     if (region_a.getRegionInfo().equals(region_b.getRegionInfo())) {
195       LOG.info("Can't merge the same region " + region_a);
196       return false;
197     }
198     if (!forcible && !HRegionInfo.areAdjacent(region_a.getRegionInfo(),
199             region_b.getRegionInfo())) {
200       String msg = "Skip merging " + this.region_a.getRegionInfo().getRegionNameAsString()
201           + " and " + this.region_b.getRegionInfo().getRegionNameAsString()
202           + ", because they are not adjacent.";
203       LOG.info(msg);
204       return false;
205     }
206     if (!this.region_a.isMergeable() || !this.region_b.isMergeable()) {
207       return false;
208     }
209     try {
210       boolean regionAHasMergeQualifier = hasMergeQualifierInMeta(services,
211           region_a.getRegionInfo().getRegionName());
212       if (regionAHasMergeQualifier ||
213           hasMergeQualifierInMeta(services, region_b.getRegionInfo().getRegionName())) {
214         LOG.debug("Region " + (regionAHasMergeQualifier ?
215               region_a.getRegionInfo().getRegionNameAsString() :
216                 region_b.getRegionInfo().getRegionNameAsString())
217             + " is not mergeable because it has merge qualifier in META");
218         return false;
219       }
220     } catch (IOException e) {
221       LOG.warn("Failed judging whether merge transaction is available for "
222               + region_a.getRegionInfo().getRegionNameAsString() + " and "
223               + region_b.getRegionInfo().getRegionNameAsString(), e);
224       return false;
225     }
226 
227     // WARN: make sure there is no parent region of the two merging regions in
228     // hbase:meta If exists, fixing up daughters would cause daughter regions(we
229     // have merged one) online again when we restart master, so we should clear
230     // the parent region to prevent the above case
231     // Since HBASE-7721, we don't need fix up daughters any more. so here do
232     // nothing
233 
234     this.mergedRegionInfo = getMergedRegionInfo(region_a.getRegionInfo(),
235         region_b.getRegionInfo());
236 
237     transition(RegionMergeTransactionPhase.PREPARED);
238     return true;
239   }
240 
241   /**
242    * Run the transaction.
243    * @param server Hosting server instance. Can be null when testing
244    * @param services Used to online/offline regions.
245    * @throws IOException If thrown, transaction failed. Call
246    *           {@link #rollback(Server, RegionServerServices)}
247    * @return merged region
248    * @throws IOException
249    * @see #rollback(Server, RegionServerServices)
250    */
251   @Override
252   public HRegion execute(final Server server,
253       final RegionServerServices services) throws IOException {
254     if (User.isHBaseSecurityEnabled(region_a.getBaseConf())) {
255       LOG.warn("Should use execute(Server, RegionServerServices, User)");
256     }
257     return execute(server, services, null);
258   }
259 
260   @Override
261   public HRegion execute(final Server server, final RegionServerServices services, User user)
262       throws IOException {
263     this.server = server;
264     this.rsServices = services;
265     useCoordinationForAssignment =
266         server == null ? true : ConfigUtil.useZKForAssignment(server.getConfiguration());
267     if (rmd == null) {
268       rmd = server != null && server.getCoordinatedStateManager() != null ?
269           ((BaseCoordinatedStateManager) server.getCoordinatedStateManager())
270             .getRegionMergeCoordination().getDefaultDetails()
271         : null;
272     }
273     if (rsCoprocessorHost == null) {
274       rsCoprocessorHost = server != null ?
275         ((HRegionServer) server).getRegionServerCoprocessorHost() : null;
276     }
277     final HRegion mergedRegion = createMergedRegion(server, services, user);
278     if (rsCoprocessorHost != null) {
279       rsCoprocessorHost.postMergeCommit(this.region_a, this.region_b, mergedRegion, user);
280     }
281     stepsAfterPONR(server, services, mergedRegion, user);
282 
283     transition(RegionMergeTransactionPhase.COMPLETED);
284 
285     return mergedRegion;
286   }
287 
288   @Deprecated
289   public void stepsAfterPONR(final Server server, final RegionServerServices services,
290       final HRegion mergedRegion) throws IOException {
291     stepsAfterPONR(server, services, mergedRegion, null);
292   }
293 
294   public void stepsAfterPONR(final Server server, final RegionServerServices services,
295       final HRegion mergedRegion, User user) throws IOException {
296     openMergedRegion(server, services, mergedRegion);
297     if (useCoordination(server)) {
298       ((BaseCoordinatedStateManager) server.getCoordinatedStateManager())
299         .getRegionMergeCoordination().completeRegionMergeTransaction(services,
300           mergedRegionInfo, region_a, region_b, rmd, mergedRegion);
301     }
302     if (rsCoprocessorHost != null) {
303       rsCoprocessorHost.postMerge(region_a, region_b, mergedRegion, user);
304     }
305   }
306 
307   /**
308    * Prepare the merged region and region files.
309    * @param server Hosting server instance. Can be null when testing
310    * @param services Used to online/offline regions.
311    * @return merged region
312    * @throws IOException If thrown, transaction failed. Call
313    *           {@link #rollback(Server, RegionServerServices)}
314    */
315   HRegion createMergedRegion(final Server server,
316       final RegionServerServices services, User user) throws IOException {
317     LOG.info("Starting merge of " + region_a + " and "
318         + region_b.getRegionInfo().getRegionNameAsString() + ", forcible=" + forcible);
319     if ((server != null && server.isStopped())
320         || (services != null && services.isStopping())) {
321       throw new IOException("Server is stopped or stopping");
322     }
323 
324     if (rsCoprocessorHost != null) {
325       boolean ret = rsCoprocessorHost.preMerge(region_a, region_b, user);
326       if (ret) {
327         throw new IOException("Coprocessor bypassing regions " + this.region_a + " "
328             + this.region_b + " merge.");
329       }
330     }
331 
332     // If true, no cluster to write meta edits to or to use coordination.
333     boolean testing = server == null ? true : server.getConfiguration()
334         .getBoolean("hbase.testing.nocluster", false);
335 
336     HRegion mergedRegion = stepsBeforePONR(server, services, testing);
337 
338     @MetaMutationAnnotation
339     final List<Mutation> metaEntries = new ArrayList<Mutation>();
340     if (rsCoprocessorHost != null) {
341       boolean ret = rsCoprocessorHost.preMergeCommit(region_a, region_b, metaEntries, user);
342 
343       if (ret) {
344         throw new IOException("Coprocessor bypassing regions " + this.region_a + " "
345             + this.region_b + " merge.");
346       }
347       try {
348         for (Mutation p : metaEntries) {
349           HRegionInfo.parseRegionName(p.getRow());
350         }
351       } catch (IOException e) {
352         LOG.error("Row key of mutation from coprocessor is not parsable as region name."
353             + "Mutations from coprocessor should only be for hbase:meta table.", e);
354         throw e;
355       }
356     }
357 
358     // This is the point of no return. Similar with SplitTransaction.
359     // IF we reach the PONR then subsequent failures need to crash out this
360     // regionserver
361     transition(RegionMergeTransactionPhase.PONR);
362 
363     // Add merged region and delete region_a and region_b
364     // as an atomic update. See HBASE-7721. This update to hbase:meta makes the region
365     // will determine whether the region is merged or not in case of failures.
366     // If it is successful, master will roll-forward, if not, master will
367     // rollback
368     if (!testing && useCoordinationForAssignment) {
369       if (metaEntries.isEmpty()) {
370         MetaTableAccessor.mergeRegions(server.getConnection(),
371           mergedRegion.getRegionInfo(), region_a.getRegionInfo(), region_b.getRegionInfo(),
372           server.getServerName(), region_a.getTableDesc().getRegionReplication(), masterSystemTime);
373       } else {
374         mergeRegionsAndPutMetaEntries(server.getConnection(),
375           mergedRegion.getRegionInfo(), region_a.getRegionInfo(), region_b.getRegionInfo(),
376           server.getServerName(), metaEntries, region_a.getTableDesc().getRegionReplication());
377       }
378     } else if (services != null && !useCoordinationForAssignment) {
379       if (!services.reportRegionStateTransition(TransitionCode.MERGE_PONR,
380           mergedRegionInfo, region_a.getRegionInfo(), region_b.getRegionInfo())) {
381         // Passed PONR, let SSH clean it up
382         throw new IOException("Failed to notify master that merge passed PONR: "
383           + region_a.getRegionInfo().getRegionNameAsString() + " and "
384           + region_b.getRegionInfo().getRegionNameAsString());
385       }
386     }
387     return mergedRegion;
388   }
389 
390   private void mergeRegionsAndPutMetaEntries(HConnection hConnection,
391       HRegionInfo mergedRegion, HRegionInfo regionA, HRegionInfo regionB,
392       ServerName serverName, List<Mutation> metaEntries,
393       int regionReplication) throws IOException {
394     prepareMutationsForMerge(mergedRegion, regionA, regionB, serverName, metaEntries,
395       regionReplication);
396     MetaTableAccessor.mutateMetaTable(hConnection, metaEntries);
397   }
398 
399   public void prepareMutationsForMerge(HRegionInfo mergedRegion, HRegionInfo regionA,
400       HRegionInfo regionB, ServerName serverName, List<Mutation> mutations,
401       int regionReplication) throws IOException {
402     HRegionInfo copyOfMerged = new HRegionInfo(mergedRegion);
403 
404     // use the maximum of what master passed us vs local time.
405     long time = Math.max(EnvironmentEdgeManager.currentTime(), masterSystemTime);
406 
407     // Put for parent
408     Put putOfMerged = MetaTableAccessor.makePutFromRegionInfo(copyOfMerged, time);
409     putOfMerged.add(HConstants.CATALOG_FAMILY, HConstants.MERGEA_QUALIFIER,
410       regionA.toByteArray());
411     putOfMerged.add(HConstants.CATALOG_FAMILY, HConstants.MERGEB_QUALIFIER,
412       regionB.toByteArray());
413     mutations.add(putOfMerged);
414     // Deletes for merging regions
415     Delete deleteA = MetaTableAccessor.makeDeleteFromRegionInfo(regionA, time);
416     Delete deleteB = MetaTableAccessor.makeDeleteFromRegionInfo(regionB, time);
417     mutations.add(deleteA);
418     mutations.add(deleteB);
419 
420     // Add empty locations for region replicas of the merged region so that number of replicas
421     // can be cached whenever the primary region is looked up from meta
422     for (int i = 1; i < regionReplication; i++) {
423       addEmptyLocation(putOfMerged, i);
424     }
425 
426     // The merged is a new region, openSeqNum = 1 is fine.
427     addLocation(putOfMerged, serverName, 1);
428   }
429 
430   public Put addLocation(final Put p, final ServerName sn, long openSeqNum) {
431     p.add(HConstants.CATALOG_FAMILY, HConstants.SERVER_QUALIFIER, Bytes
432         .toBytes(sn.getHostAndPort()));
433     p.add(HConstants.CATALOG_FAMILY, HConstants.STARTCODE_QUALIFIER, Bytes.toBytes(sn
434         .getStartcode()));
435     p.add(HConstants.CATALOG_FAMILY, HConstants.SEQNUM_QUALIFIER, Bytes.toBytes(openSeqNum));
436     return p;
437   }
438 
439   private static Put addEmptyLocation(final Put p, int replicaId) {
440     p.addImmutable(HConstants.CATALOG_FAMILY, MetaTableAccessor.getServerColumn(replicaId), null);
441     p.addImmutable(HConstants.CATALOG_FAMILY, MetaTableAccessor.getStartCodeColumn(replicaId),
442       null);
443     p.addImmutable(HConstants.CATALOG_FAMILY, MetaTableAccessor.getSeqNumColumn(replicaId), null);
444     return p;
445   }
446 
447   public HRegion stepsBeforePONR(final Server server, final RegionServerServices services,
448       boolean testing) throws IOException {
449     if (rmd == null) {
450       rmd = server != null && server.getCoordinatedStateManager() != null ?
451           ((BaseCoordinatedStateManager) server.getCoordinatedStateManager())
452             .getRegionMergeCoordination().getDefaultDetails()
453         : null;
454     }
455 
456     // If server doesn't have a coordination state manager, don't do coordination actions.
457     if (useCoordination(server)) {
458       try {
459         ((BaseCoordinatedStateManager) server.getCoordinatedStateManager())
460             .getRegionMergeCoordination().startRegionMergeTransaction(mergedRegionInfo,
461               server.getServerName(), region_a.getRegionInfo(), region_b.getRegionInfo());
462       } catch (IOException e) {
463         throw new IOException("Failed to start region merge transaction for "
464             + this.mergedRegionInfo.getRegionNameAsString(), e);
465       }
466     } else if (services != null && !useCoordinationForAssignment) {
467       if (!services.reportRegionStateTransition(TransitionCode.READY_TO_MERGE,
468           mergedRegionInfo, region_a.getRegionInfo(), region_b.getRegionInfo())) {
469         throw new IOException("Failed to get ok from master to merge "
470           + region_a.getRegionInfo().getRegionNameAsString() + " and "
471           + region_b.getRegionInfo().getRegionNameAsString());
472       }
473     }
474     transition(RegionMergeTransactionPhase.SET_MERGING);
475     if (useCoordination(server)) {
476       // After creating the merge node, wait for master to transition it
477       // from PENDING_MERGE to MERGING so that we can move on. We want master
478       // knows about it and won't transition any region which is merging.
479       ((BaseCoordinatedStateManager) server.getCoordinatedStateManager())
480         .getRegionMergeCoordination().waitForRegionMergeTransaction(services,
481           mergedRegionInfo, region_a, region_b, rmd);
482     }
483 
484     this.region_a.getRegionFileSystem().createMergesDir();
485     transition(RegionMergeTransactionPhase.CREATED_MERGE_DIR);
486 
487     Map<byte[], List<StoreFile>> hstoreFilesOfRegionA = closeAndOfflineRegion(
488         services, this.region_a, true, testing);
489     Map<byte[], List<StoreFile>> hstoreFilesOfRegionB = closeAndOfflineRegion(
490         services, this.region_b, false, testing);
491 
492     assert hstoreFilesOfRegionA != null && hstoreFilesOfRegionB != null;
493 
494 
495     //
496     // mergeStoreFiles creates merged region dirs under the region_a merges dir
497     // Nothing to unroll here if failure -- clean up of CREATE_MERGE_DIR will
498     // clean this up.
499     mergeStoreFiles(hstoreFilesOfRegionA, hstoreFilesOfRegionB);
500 
501     if (useCoordination(server)) {
502       try {
503         // Do the final check in case any merging region is moved somehow. If so, the transition
504         // will fail.
505         ((BaseCoordinatedStateManager) server.getCoordinatedStateManager())
506             .getRegionMergeCoordination().confirmRegionMergeTransaction(this.mergedRegionInfo,
507               region_a.getRegionInfo(), region_b.getRegionInfo(), server.getServerName(), rmd);
508       } catch (IOException e) {
509         throw new IOException("Failed setting MERGING on "
510             + this.mergedRegionInfo.getRegionNameAsString(), e);
511       }
512     }
513 
514     // Log to the journal that we are creating merged region. We could fail
515     // halfway through. If we do, we could have left
516     // stuff in fs that needs cleanup -- a storefile or two. Thats why we
517     // add entry to journal BEFORE rather than AFTER the change.
518     transition(RegionMergeTransactionPhase.STARTED_MERGED_REGION_CREATION);
519     HRegion mergedRegion = createMergedRegionFromMerges(this.region_a,
520         this.region_b, this.mergedRegionInfo);
521     return mergedRegion;
522   }
523 
524   /**
525    * Create a merged region from the merges directory under region a. In order
526    * to mock it for tests, place it with a new method.
527    * @param a hri of region a
528    * @param b hri of region b
529    * @param mergedRegion hri of merged region
530    * @return merged HRegion.
531    * @throws IOException
532    */
533   HRegion createMergedRegionFromMerges(final HRegion a, final HRegion b,
534       final HRegionInfo mergedRegion) throws IOException {
535     return a.createMergedRegionFromMerges(mergedRegion, b);
536   }
537 
538   /**
539    * Close the merging region and offline it in regionserver
540    * @param services
541    * @param region
542    * @param isRegionA true if it is merging region a, false if it is region b
543    * @param testing true if it is testing
544    * @return a map of family name to list of store files
545    * @throws IOException
546    */
547   private Map<byte[], List<StoreFile>> closeAndOfflineRegion(
548       final RegionServerServices services, final HRegion region,
549       final boolean isRegionA, final boolean testing) throws IOException {
550     Map<byte[], List<StoreFile>> hstoreFilesToMerge = null;
551     Exception exceptionToThrow = null;
552     try {
553       hstoreFilesToMerge = region.close(false);
554     } catch (Exception e) {
555       exceptionToThrow = e;
556     }
557     if (exceptionToThrow == null && hstoreFilesToMerge == null) {
558       // The region was closed by a concurrent thread. We can't continue
559       // with the merge, instead we must just abandon the merge. If we
560       // reopen or merge this could cause problems because the region has
561       // probably already been moved to a different server, or is in the
562       // process of moving to a different server.
563       exceptionToThrow = closedByOtherException;
564     }
565     if (exceptionToThrow != closedByOtherException) {
566       transition(isRegionA ? RegionMergeTransactionPhase.CLOSED_REGION_A
567           : RegionMergeTransactionPhase.CLOSED_REGION_B);
568     }
569     if (exceptionToThrow != null) {
570       if (exceptionToThrow instanceof IOException)
571         throw (IOException) exceptionToThrow;
572       throw new IOException(exceptionToThrow);
573     }
574 
575     if (!testing) {
576       services.removeFromOnlineRegions(region, null);
577     }
578     transition(isRegionA ? RegionMergeTransactionPhase.OFFLINED_REGION_A
579         : RegionMergeTransactionPhase.OFFLINED_REGION_B);
580     return hstoreFilesToMerge;
581   }
582 
583   /**
584    * Get merged region info through the specified two regions
585    * @param a merging region A
586    * @param b merging region B
587    * @return the merged region info
588    */
589   public static HRegionInfo getMergedRegionInfo(final HRegionInfo a,
590       final HRegionInfo b) {
591     long rid = EnvironmentEdgeManager.currentTime();
592     // Regionid is timestamp. Merged region's id can't be less than that of
593     // merging regions else will insert at wrong location in hbase:meta
594     if (rid < a.getRegionId() || rid < b.getRegionId()) {
595       LOG.warn("Clock skew; merging regions id are " + a.getRegionId()
596           + " and " + b.getRegionId() + ", but current time here is " + rid);
597       rid = Math.max(a.getRegionId(), b.getRegionId()) + 1;
598     }
599 
600     byte[] startKey = null;
601     byte[] endKey = null;
602     // Choose the smaller as start key
603     if (a.compareTo(b) <= 0) {
604       startKey = a.getStartKey();
605     } else {
606       startKey = b.getStartKey();
607     }
608     // Choose the bigger as end key
609     if (Bytes.equals(a.getEndKey(), HConstants.EMPTY_BYTE_ARRAY)
610         || (!Bytes.equals(b.getEndKey(), HConstants.EMPTY_BYTE_ARRAY)
611             && Bytes.compareTo(a.getEndKey(), b.getEndKey()) > 0)) {
612       endKey = a.getEndKey();
613     } else {
614       endKey = b.getEndKey();
615     }
616 
617     // Merged region is sorted between two merging regions in META
618     HRegionInfo mergedRegionInfo = new HRegionInfo(a.getTable(), startKey,
619         endKey, false, rid);
620     return mergedRegionInfo;
621   }
622 
623   /**
624    * Perform time consuming opening of the merged region.
625    * @param server Hosting server instance. Can be null when testing
626    * @param services Used to online/offline regions.
627    * @param merged the merged region
628    * @throws IOException If thrown, transaction failed. Call
629    *           {@link #rollback(Server, RegionServerServices)}
630    */
631   void openMergedRegion(final Server server,
632       final RegionServerServices services, HRegion merged) throws IOException {
633     boolean stopped = server != null && server.isStopped();
634     boolean stopping = services != null && services.isStopping();
635     if (stopped || stopping) {
636       LOG.info("Not opening merged region  " + merged.getRegionInfo().getRegionNameAsString()
637           + " because stopping=" + stopping + ", stopped=" + stopped);
638       return;
639     }
640     HRegionInfo hri = merged.getRegionInfo();
641     LoggingProgressable reporter = server == null ? null
642         : new LoggingProgressable(hri, server.getConfiguration().getLong(
643             "hbase.regionserver.regionmerge.open.log.interval", 10000));
644     merged.openHRegion(reporter);
645 
646     if (services != null) {
647       try {
648         if (useCoordinationForAssignment) {
649           services.postOpenDeployTasks(merged);
650         } else if (!services.reportRegionStateTransition(TransitionCode.MERGED,
651             mergedRegionInfo, region_a.getRegionInfo(), region_b.getRegionInfo())) {
652           throw new IOException("Failed to report merged region to master: "
653             + mergedRegionInfo.getShortNameToLog());
654         }
655         services.addToOnlineRegions(merged);
656       } catch (KeeperException ke) {
657         throw new IOException(ke);
658       }
659     }
660 
661   }
662 
663   /**
664    * Create reference file(s) of merging regions under the region_a merges dir
665    * @param hstoreFilesOfRegionA
666    * @param hstoreFilesOfRegionB
667    * @throws IOException
668    */
669   private void mergeStoreFiles(
670       Map<byte[], List<StoreFile>> hstoreFilesOfRegionA,
671       Map<byte[], List<StoreFile>> hstoreFilesOfRegionB)
672       throws IOException {
673     // Create reference file(s) of region A in mergdir
674     HRegionFileSystem fs_a = this.region_a.getRegionFileSystem();
675     for (Map.Entry<byte[], List<StoreFile>> entry : hstoreFilesOfRegionA
676         .entrySet()) {
677       String familyName = Bytes.toString(entry.getKey());
678       for (StoreFile storeFile : entry.getValue()) {
679         fs_a.mergeStoreFile(this.mergedRegionInfo, familyName, storeFile,
680             this.mergesdir);
681       }
682     }
683     // Create reference file(s) of region B in mergedir
684     HRegionFileSystem fs_b = this.region_b.getRegionFileSystem();
685     for (Map.Entry<byte[], List<StoreFile>> entry : hstoreFilesOfRegionB
686         .entrySet()) {
687       String familyName = Bytes.toString(entry.getKey());
688       for (StoreFile storeFile : entry.getValue()) {
689         fs_b.mergeStoreFile(this.mergedRegionInfo, familyName, storeFile,
690             this.mergesdir);
691       }
692     }
693   }
694 
695   /**
696    * @param server Hosting server instance (May be null when testing).
697    * @param services Services of regionserver, used to online regions.
698    * @throws IOException If thrown, rollback failed. Take drastic action.
699    * @return True if we successfully rolled back, false if we got to the point
700    *         of no return and so now need to abort the server to minimize
701    *         damage.
702    */
703   @Override
704   @SuppressWarnings("deprecation")
705   public boolean rollback(final Server server,
706       final RegionServerServices services) throws IOException {
707     if (User.isHBaseSecurityEnabled(region_a.getBaseConf())) {
708       LOG.warn("Should use execute(Server, RegionServerServices, User)");
709     }
710     return rollback(server, services, null);
711   }
712 
713   @Override
714   public boolean rollback(final Server server,
715       final RegionServerServices services, User user) throws IOException {
716     assert this.mergedRegionInfo != null;
717     // Coprocessor callback
718     if (rsCoprocessorHost != null) {
719       rsCoprocessorHost.preRollBackMerge(region_a, region_b, user);
720     }
721 
722     boolean result = true;
723     ListIterator<JournalEntry> iterator = this.journal
724         .listIterator(this.journal.size());
725     // Iterate in reverse.
726     while (iterator.hasPrevious()) {
727       JournalEntry je = iterator.previous();
728 
729       transition(je.getPhase(), true);
730 
731       switch (je.getPhase()) {
732 
733         case SET_MERGING:
734         if (useCoordination(server)) {
735           ((BaseCoordinatedStateManager) server.getCoordinatedStateManager())
736               .getRegionMergeCoordination().clean(this.mergedRegionInfo);
737           } else if (services != null && !useCoordinationForAssignment
738               && !services.reportRegionStateTransition(TransitionCode.MERGE_REVERTED,
739                   mergedRegionInfo, region_a.getRegionInfo(), region_b.getRegionInfo())) {
740             return false;
741         }
742           break;
743 
744         case CREATED_MERGE_DIR:
745           this.region_a.writestate.writesEnabled = true;
746           this.region_b.writestate.writesEnabled = true;
747           this.region_a.getRegionFileSystem().cleanupMergesDir();
748           break;
749 
750         case CLOSED_REGION_A:
751           try {
752             // So, this returns a seqid but if we just closed and then reopened,
753             // we should be ok. On close, we flushed using sequenceid obtained
754             // from hosting regionserver so no need to propagate the sequenceid
755             // returned out of initialize below up into regionserver as we
756             // normally do.
757             this.region_a.initialize();
758           } catch (IOException e) {
759             LOG.error("Failed rollbacking CLOSED_REGION_A of region "
760                 + this.region_a.getRegionInfo().getRegionNameAsString(), e);
761             throw new RuntimeException(e);
762           }
763           break;
764 
765         case OFFLINED_REGION_A:
766           if (services != null)
767             services.addToOnlineRegions(this.region_a);
768           break;
769 
770         case CLOSED_REGION_B:
771           try {
772             this.region_b.initialize();
773           } catch (IOException e) {
774             LOG.error("Failed rollbacking CLOSED_REGION_A of region "
775                 + this.region_b.getRegionInfo().getRegionNameAsString(), e);
776             throw new RuntimeException(e);
777           }
778           break;
779 
780         case OFFLINED_REGION_B:
781           if (services != null)
782             services.addToOnlineRegions(this.region_b);
783           break;
784 
785         case STARTED_MERGED_REGION_CREATION:
786           this.region_a.getRegionFileSystem().cleanupMergedRegion(
787               this.mergedRegionInfo);
788           break;
789 
790         case PONR:
791           // We got to the point-of-no-return so we need to just abort. Return
792           // immediately. Do not clean up created merged regions.
793           return false;
794 
795         // Informational only cases
796         case STARTED:
797         case PREPARED:
798         case COMPLETED:
799           break;
800 
801         default:
802           throw new RuntimeException("Unhandled journal entry: " + je);
803       }
804     }
805     // Coprocessor callback
806     if (rsCoprocessorHost != null) {
807       rsCoprocessorHost.postRollBackMerge(region_a, region_b, user);
808     }
809 
810     return result;
811   }
812 
813   @Override
814   public HRegionInfo getMergedRegionInfo() {
815     return this.mergedRegionInfo;
816   }
817 
818   // For unit testing.
819   Path getMergesDir() {
820     return this.mergesdir;
821   }
822 
823   private boolean useCoordination(final Server server) {
824     return server != null && useCoordinationForAssignment
825         && server.getCoordinatedStateManager() != null;
826   }
827 
828 
829 
830   /**
831    * Checks if the given region has merge qualifier in hbase:meta
832    * @param services
833    * @param regionName name of specified region
834    * @return true if the given region has merge qualifier in META.(It will be
835    *         cleaned by CatalogJanitor)
836    * @throws IOException
837    */
838   boolean hasMergeQualifierInMeta(final RegionServerServices services,
839       final byte[] regionName) throws IOException {
840     if (services == null) return false;
841     // Get merge regions if it is a merged region and already has merge
842     // qualifier
843     Pair<HRegionInfo, HRegionInfo> mergeRegions = MetaTableAccessor
844         .getRegionsFromMergeQualifier(services.getConnection(), regionName);
845     if (mergeRegions != null &&
846         (mergeRegions.getFirst() != null || mergeRegions.getSecond() != null)) {
847       // It has merge qualifier
848       return true;
849     }
850     return false;
851   }
852 
853   @Override
854   public List<JournalEntry> getJournal() {
855     return journal;
856   }
857 
858   @Override
859   public RegionMergeTransaction registerTransactionListener(TransactionListener listener) {
860     listeners.add(listener);
861     return this;
862   }
863 
864   @Override
865   public Server getServer() {
866     return server;
867   }
868 
869   @Override
870   public RegionServerServices getRegionServerServices() {
871     return rsServices;
872   }
873 
874   @Override
875   public String toString() {
876     StringBuilder sb = new StringBuilder();
877     for (int i = 0; i < journal.size(); i++) {
878       JournalEntry je = journal.get(i);
879       sb.append(je.toString());
880       if (i != 0) {
881         JournalEntry jep = journal.get(i-1);
882         long delta = je.getTimeStamp() - jep.getTimeStamp();
883         if (delta != 0) {
884           sb.append(" (+" + delta + " ms)");
885         }
886       }
887       sb.append("\n");
888     }
889     return sb.toString();
890   }
891 }