001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.master.janitor;
019
020import static org.apache.hadoop.hbase.util.HFileArchiveTestingUtil.assertArchiveEqualToOriginal;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertFalse;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024import static org.mockito.ArgumentMatchers.any;
025import static org.mockito.Mockito.doAnswer;
026import static org.mockito.Mockito.doReturn;
027import static org.mockito.Mockito.doThrow;
028import static org.mockito.Mockito.spy;
029import static org.mockito.Mockito.when;
030
031import java.io.IOException;
032import java.util.ArrayList;
033import java.util.List;
034import java.util.Map;
035import java.util.Objects;
036import java.util.SortedMap;
037import java.util.TreeMap;
038import java.util.concurrent.atomic.AtomicBoolean;
039import org.apache.hadoop.fs.FSDataOutputStream;
040import org.apache.hadoop.fs.FileStatus;
041import org.apache.hadoop.fs.FileSystem;
042import org.apache.hadoop.fs.Path;
043import org.apache.hadoop.hbase.HBaseTestingUtil;
044import org.apache.hadoop.hbase.HConstants;
045import org.apache.hadoop.hbase.MetaMockingUtil;
046import org.apache.hadoop.hbase.TableName;
047import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
048import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
049import org.apache.hadoop.hbase.client.RegionInfo;
050import org.apache.hadoop.hbase.client.RegionInfoBuilder;
051import org.apache.hadoop.hbase.client.Result;
052import org.apache.hadoop.hbase.client.TableDescriptor;
053import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
054import org.apache.hadoop.hbase.io.Reference;
055import org.apache.hadoop.hbase.master.MasterFileSystem;
056import org.apache.hadoop.hbase.master.MasterServices;
057import org.apache.hadoop.hbase.master.assignment.MockMasterServices;
058import org.apache.hadoop.hbase.master.janitor.CatalogJanitor.SplitParentFirstComparator;
059import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility;
060import org.apache.hadoop.hbase.regionserver.ChunkCreator;
061import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
062import org.apache.hadoop.hbase.regionserver.MemStoreLAB;
063import org.apache.hadoop.hbase.regionserver.StoreContext;
064import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTracker;
065import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFactory;
066import org.apache.hadoop.hbase.testclassification.MasterTests;
067import org.apache.hadoop.hbase.testclassification.MediumTests;
068import org.apache.hadoop.hbase.util.Bytes;
069import org.apache.hadoop.hbase.util.CommonFSUtils;
070import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
071import org.apache.hadoop.hbase.util.HFileArchiveUtil;
072import org.junit.jupiter.api.AfterEach;
073import org.junit.jupiter.api.BeforeAll;
074import org.junit.jupiter.api.BeforeEach;
075import org.junit.jupiter.api.Tag;
076import org.junit.jupiter.api.Test;
077import org.junit.jupiter.api.TestInfo;
078import org.slf4j.Logger;
079import org.slf4j.LoggerFactory;
080
081@Tag(MasterTests.TAG)
082@Tag(MediumTests.TAG)
083public class TestCatalogJanitor {
084
085  private static final Logger LOG = LoggerFactory.getLogger(TestCatalogJanitor.class);
086
087  private static final HBaseTestingUtil HTU = new HBaseTestingUtil();
088
089  private String currentTestMethod;
090
091  private MockMasterServices masterServices;
092  private CatalogJanitor janitor;
093
094  @BeforeAll
095  public static void beforeClass() throws Exception {
096    ChunkCreator.initialize(MemStoreLAB.CHUNK_SIZE_DEFAULT, false, 0, 0, 0, null,
097      MemStoreLAB.INDEX_CHUNK_SIZE_PERCENTAGE_DEFAULT);
098  }
099
100  @BeforeEach
101  public void setup(TestInfo testInfo) throws Exception {
102    this.currentTestMethod = testInfo.getTestMethod().get().getName();
103    setRootDirAndCleanIt(HTU, this.currentTestMethod);
104    this.masterServices = new MockMasterServices(HTU.getConfiguration());
105    this.masterServices.start(10, null);
106    this.janitor = new CatalogJanitor(masterServices);
107  }
108
109  @AfterEach
110  public void teardown() {
111    this.janitor.shutdown(true);
112    this.masterServices.stop("DONE");
113  }
114
115  private RegionInfo createRegionInfo(TableName tableName, byte[] startKey, byte[] endKey) {
116    return createRegionInfo(tableName, startKey, endKey, false);
117  }
118
119  private RegionInfo createRegionInfo(TableName tableName, byte[] startKey, byte[] endKey,
120    boolean split) {
121    return RegionInfoBuilder.newBuilder(tableName).setStartKey(startKey).setEndKey(endKey)
122      .setSplit(split).build();
123  }
124
125  @Test
126  public void testCleanMerge() throws IOException {
127    TableDescriptor td = createTableDescriptorForCurrentMethod();
128    // Create regions.
129    RegionInfo merged =
130      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("eee"));
131    RegionInfo parenta =
132      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("ccc"));
133    RegionInfo parentb =
134      createRegionInfo(td.getTableName(), Bytes.toBytes("ccc"), Bytes.toBytes("eee"));
135
136    List<RegionInfo> parents = new ArrayList<>();
137    parents.add(parenta);
138    parents.add(parentb);
139
140    Path rootdir = this.masterServices.getMasterFileSystem().getRootDir();
141    Path tabledir = CommonFSUtils.getTableDir(rootdir, td.getTableName());
142    Path storedir =
143      HRegionFileSystem.getStoreHomedir(tabledir, merged, td.getColumnFamilies()[0].getName());
144
145    Path parentaRef =
146      createMergeReferenceFile(storedir, tabledir, td.getColumnFamilies()[0], merged, parenta);
147    Path parentbRef =
148      createMergeReferenceFile(storedir, tabledir, td.getColumnFamilies()[0], merged, parentb);
149
150    // references exist, should not clean
151    assertFalse(CatalogJanitor.cleanMergeRegion(masterServices, merged, parents));
152
153    masterServices.getMasterFileSystem().getFileSystem().delete(parentaRef, false);
154
155    // one reference still exists, should not clean
156    assertFalse(CatalogJanitor.cleanMergeRegion(masterServices, merged, parents));
157
158    masterServices.getMasterFileSystem().getFileSystem().delete(parentbRef, false);
159
160    // all references removed, should clean
161    assertTrue(CatalogJanitor.cleanMergeRegion(masterServices, merged, parents));
162  }
163
164  @Test
165  public void testDontCleanMergeIfFileSystemException() throws IOException {
166    TableDescriptor td = createTableDescriptorForCurrentMethod();
167    // Create regions.
168    RegionInfo merged =
169      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("eee"));
170    RegionInfo parenta =
171      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("ccc"));
172    RegionInfo parentb =
173      createRegionInfo(td.getTableName(), Bytes.toBytes("ccc"), Bytes.toBytes("eee"));
174
175    List<RegionInfo> parents = new ArrayList<>();
176    parents.add(parenta);
177    parents.add(parentb);
178
179    Path rootdir = this.masterServices.getMasterFileSystem().getRootDir();
180    Path tabledir = CommonFSUtils.getTableDir(rootdir, td.getTableName());
181    Path storedir =
182      HRegionFileSystem.getStoreHomedir(tabledir, merged, td.getColumnFamilies()[0].getName());
183    createMergeReferenceFile(storedir, tabledir, td.getColumnFamilies()[0], merged, parenta);
184
185    MasterServices mockedMasterServices = spy(masterServices);
186    MasterFileSystem mockedMasterFileSystem = spy(masterServices.getMasterFileSystem());
187    FileSystem mockedFileSystem = spy(masterServices.getMasterFileSystem().getFileSystem());
188
189    when(mockedMasterServices.getMasterFileSystem()).thenReturn(mockedMasterFileSystem);
190    when(mockedMasterFileSystem.getFileSystem()).thenReturn(mockedFileSystem);
191
192    // throw on the first exists check
193    doThrow(new IOException("Some exception")).when(mockedFileSystem).exists(any());
194
195    assertFalse(CatalogJanitor.cleanMergeRegion(mockedMasterServices, merged, parents));
196
197    // throw on the second exists check (within HRegionfileSystem)
198    AtomicBoolean returned = new AtomicBoolean(false);
199    doAnswer(invocationOnMock -> {
200      if (!returned.get()) {
201        returned.set(true);
202        return masterServices.getMasterFileSystem().getFileSystem()
203          .exists(invocationOnMock.getArgument(0));
204      }
205      throw new IOException("Some exception");
206    }).when(mockedFileSystem).exists(any());
207
208    assertFalse(CatalogJanitor.cleanMergeRegion(mockedMasterServices, merged, parents));
209  }
210
211  private Path createMergeReferenceFile(Path storeDir, Path tableDir,
212    ColumnFamilyDescriptor columnFamilyDescriptor, RegionInfo mergedRegion, RegionInfo parentRegion)
213    throws IOException {
214    Reference ref = Reference.createTopReference(mergedRegion.getStartKey());
215    long now = EnvironmentEdgeManager.currentTime();
216    // Reference name has this format: StoreFile#REF_NAME_PARSER
217    Path p = new Path(storeDir, Long.toString(now) + "." + parentRegion.getEncodedName());
218    FileSystem fs = this.masterServices.getMasterFileSystem().getFileSystem();
219    HRegionFileSystem mergedRegionFS =
220      HRegionFileSystem.create(fs.getConf(), fs, tableDir, mergedRegion);
221    StoreContext storeContext =
222      StoreContext.getBuilder().withColumnFamilyDescriptor(columnFamilyDescriptor)
223        .withFamilyStoreDirectoryPath(storeDir).withRegionFileSystem(mergedRegionFS).build();
224    StoreFileTracker sft = StoreFileTrackerFactory.create(fs.getConf(), false, storeContext);
225    sft.createReference(ref, p);
226    return p;
227  }
228
229  /**
230   * Test clearing a split parent.
231   */
232  @Test
233  public void testCleanParent() throws IOException, InterruptedException {
234    TableDescriptor td = createTableDescriptorForCurrentMethod();
235    // Create regions.
236    RegionInfo parent =
237      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("eee"));
238    RegionInfo splita =
239      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("ccc"));
240    RegionInfo splitb =
241      createRegionInfo(td.getTableName(), Bytes.toBytes("ccc"), Bytes.toBytes("eee"));
242    // Test that when both daughter regions are in place, that we do not remove the parent.
243    Result r = createResult(parent, splita, splitb);
244    // Add a reference under splitA directory so we don't clear out the parent.
245    Path rootdir = this.masterServices.getMasterFileSystem().getRootDir();
246    Path tabledir = CommonFSUtils.getTableDir(rootdir, td.getTableName());
247    Path parentdir = new Path(tabledir, parent.getEncodedName());
248    Path storedir =
249      HRegionFileSystem.getStoreHomedir(tabledir, splita, td.getColumnFamilies()[0].getName());
250    Reference ref = Reference.createTopReference(Bytes.toBytes("ccc"));
251    long now = EnvironmentEdgeManager.currentTime();
252    // Reference name has this format: StoreFile#REF_NAME_PARSER
253    Path p = new Path(storedir, Long.toString(now) + "." + parent.getEncodedName());
254    FileSystem fs = this.masterServices.getMasterFileSystem().getFileSystem();
255    HRegionFileSystem regionFS =
256      HRegionFileSystem.create(this.masterServices.getConfiguration(), fs, tabledir, splita);
257    StoreContext storeContext =
258      StoreContext.getBuilder().withColumnFamilyDescriptor(td.getColumnFamilies()[0])
259        .withFamilyStoreDirectoryPath(storedir).withRegionFileSystem(regionFS).build();
260    StoreFileTracker sft =
261      StoreFileTrackerFactory.create(this.masterServices.getConfiguration(), false, storeContext);
262    sft.createReference(ref, p);
263    assertTrue(fs.exists(p));
264    LOG.info("Created reference " + p);
265    // Add a parentdir for kicks so can check it gets removed by the catalogjanitor.
266    fs.mkdirs(parentdir);
267    assertFalse(CatalogJanitor.cleanParent(masterServices, parent, r));
268    ProcedureTestingUtility.waitAllProcedures(masterServices.getMasterProcedureExecutor());
269    assertTrue(fs.exists(parentdir));
270    // Remove the reference file and try again.
271    assertTrue(fs.delete(p, true));
272    assertTrue(CatalogJanitor.cleanParent(masterServices, parent, r));
273    // Parent cleanup is run async as a procedure. Make sure parentdir is removed.
274    ProcedureTestingUtility.waitAllProcedures(masterServices.getMasterProcedureExecutor());
275    assertTrue(!fs.exists(parentdir));
276  }
277
278  /**
279   * Make sure parent gets cleaned up even if daughter is cleaned up before it.
280   */
281  @Test
282  public void testParentCleanedEvenIfDaughterGoneFirst() throws IOException, InterruptedException {
283    parentWithSpecifiedEndKeyCleanedEvenIfDaughterGoneFirst(this.currentTestMethod,
284      Bytes.toBytes("eee"));
285  }
286
287  /**
288   * Make sure last parent with empty end key gets cleaned up even if daughter is cleaned up before
289   * it.
290   */
291  @Test
292  public void testLastParentCleanedEvenIfDaughterGoneFirst()
293    throws IOException, InterruptedException {
294    parentWithSpecifiedEndKeyCleanedEvenIfDaughterGoneFirst(this.currentTestMethod, new byte[0]);
295  }
296
297  /**
298   * @return A TableDescriptor with a tableName of current method name and a column family that is
299   *         MockMasterServices.DEFAULT_COLUMN_FAMILY_NAME)
300   */
301  private TableDescriptor createTableDescriptorForCurrentMethod() {
302    ColumnFamilyDescriptor columnFamilyDescriptor = ColumnFamilyDescriptorBuilder
303      .newBuilder(Bytes.toBytes(MockMasterServices.DEFAULT_COLUMN_FAMILY_NAME)).build();
304    return TableDescriptorBuilder.newBuilder(TableName.valueOf(this.currentTestMethod))
305      .setColumnFamily(columnFamilyDescriptor).build();
306  }
307
308  /**
309   * Make sure parent with specified end key gets cleaned up even if daughter is cleaned up before
310   * it.
311   * @param rootDir    the test case name, used as the HBase testing utility root
312   * @param lastEndKey the end key of the split parent
313   */
314  private void parentWithSpecifiedEndKeyCleanedEvenIfDaughterGoneFirst(final String rootDir,
315    final byte[] lastEndKey) throws IOException, InterruptedException {
316    TableDescriptor td = createTableDescriptorForCurrentMethod();
317    // Create regions: aaa->{lastEndKey}, aaa->ccc, aaa->bbb, bbb->ccc, etc.
318    RegionInfo parent = createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), lastEndKey);
319    // Sleep a second else the encoded name on these regions comes out
320    // same for all with same start key and made in same second.
321    Thread.sleep(1001);
322
323    // Daughter a
324    RegionInfo splita =
325      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("ccc"));
326    Thread.sleep(1001);
327    // Make daughters of daughter a; splitaa and splitab.
328    RegionInfo splitaa =
329      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("bbb"));
330    RegionInfo splitab =
331      createRegionInfo(td.getTableName(), Bytes.toBytes("bbb"), Bytes.toBytes("ccc"));
332
333    // Daughter b
334    RegionInfo splitb = createRegionInfo(td.getTableName(), Bytes.toBytes("ccc"), lastEndKey);
335    Thread.sleep(1001);
336    // Make Daughters of daughterb; splitba and splitbb.
337    RegionInfo splitba =
338      createRegionInfo(td.getTableName(), Bytes.toBytes("ccc"), Bytes.toBytes("ddd"));
339    RegionInfo splitbb = createRegionInfo(td.getTableName(), Bytes.toBytes("ddd"), lastEndKey);
340
341    // First test that our Comparator works right up in CatalogJanitor.
342    SortedMap<RegionInfo, Result> regions =
343      new TreeMap<>(new CatalogJanitor.SplitParentFirstComparator());
344    // Now make sure that this regions map sorts as we expect it to.
345    regions.put(parent, createResult(parent, splita, splitb));
346    regions.put(splitb, createResult(splitb, splitba, splitbb));
347    regions.put(splita, createResult(splita, splitaa, splitab));
348    // Assert its properly sorted.
349    int index = 0;
350    for (Map.Entry<RegionInfo, Result> e : regions.entrySet()) {
351      if (index == 0) {
352        assertTrue(e.getKey().getEncodedName().equals(parent.getEncodedName()));
353      } else if (index == 1) {
354        assertTrue(e.getKey().getEncodedName().equals(splita.getEncodedName()));
355      } else if (index == 2) {
356        assertTrue(e.getKey().getEncodedName().equals(splitb.getEncodedName()));
357      }
358      index++;
359    }
360
361    // Now play around with the cleanParent function. Create a ref from splita up to the parent.
362    Path splitaRef =
363      createReferences(this.masterServices, td, parent, splita, Bytes.toBytes("ccc"), false);
364    // Make sure actual super parent sticks around because splita has a ref.
365    assertFalse(CatalogJanitor.cleanParent(masterServices, parent, regions.get(parent)));
366
367    // splitba, and split bb, do not have dirs in fs. That means that if
368    // we test splitb, it should get cleaned up.
369    assertTrue(CatalogJanitor.cleanParent(masterServices, splitb, regions.get(splitb)));
370
371    // Now remove ref from splita to parent... so parent can be let go and so
372    // the daughter splita can be split (can't split if still references).
373    // BUT make the timing such that the daughter gets cleaned up before we
374    // can get a chance to let go of the parent.
375    FileSystem fs = FileSystem.get(HTU.getConfiguration());
376    assertTrue(fs.delete(splitaRef, true));
377    // Create the refs from daughters of splita.
378    Path splitaaRef =
379      createReferences(this.masterServices, td, splita, splitaa, Bytes.toBytes("bbb"), false);
380    Path splitabRef =
381      createReferences(this.masterServices, td, splita, splitab, Bytes.toBytes("bbb"), true);
382
383    // Test splita. It should stick around because references from splitab, etc.
384    assertFalse(CatalogJanitor.cleanParent(masterServices, splita, regions.get(splita)));
385
386    // Now clean up parent daughter first. Remove references from its daughters.
387    assertTrue(fs.delete(splitaaRef, true));
388    assertTrue(fs.delete(splitabRef, true));
389    assertTrue(CatalogJanitor.cleanParent(masterServices, splita, regions.get(splita)));
390
391    // Super parent should get cleaned up now both splita and splitb are gone.
392    assertTrue(CatalogJanitor.cleanParent(masterServices, parent, regions.get(parent)));
393  }
394
395  /**
396   * CatalogJanitor.scan() should not clean parent regions if their own parents are still
397   * referencing them. This ensures that grandparent regions do not point to deleted parent regions.
398   */
399  @Test
400  public void testScanDoesNotCleanRegionsWithExistingParents() throws Exception {
401    TableDescriptor td = createTableDescriptorForCurrentMethod();
402    // Create regions: aaa->{lastEndKey}, aaa->ccc, aaa->bbb, bbb->ccc, etc.
403
404    // Parent
405    RegionInfo parent =
406      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), HConstants.EMPTY_BYTE_ARRAY, true);
407    // Sleep a second else the encoded name on these regions comes out
408    // same for all with same start key and made in same second.
409    Thread.sleep(1001);
410
411    // Daughter a
412    RegionInfo splita =
413      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("ccc"), true);
414    Thread.sleep(1001);
415
416    // Make daughters of daughter a; splitaa and splitab.
417    RegionInfo splitaa =
418      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("bbb"), false);
419    RegionInfo splitab =
420      createRegionInfo(td.getTableName(), Bytes.toBytes("bbb"), Bytes.toBytes("ccc"), false);
421
422    // Daughter b
423    RegionInfo splitb =
424      createRegionInfo(td.getTableName(), Bytes.toBytes("ccc"), HConstants.EMPTY_BYTE_ARRAY);
425    Thread.sleep(1001);
426
427    // Parent has daughters splita and splitb. Splita has daughters splitaa and splitab.
428    final Map<RegionInfo, Result> splitParents = new TreeMap<>(new SplitParentFirstComparator());
429    splitParents.put(parent, createResult(parent, splita, splitb));
430    // simulate that splita goes offline when it is split
431    splita = RegionInfoBuilder.newBuilder(splita).setOffline(true).build();
432    splitParents.put(splita, createResult(splita, splitaa, splitab));
433
434    final Map<RegionInfo, Result> mergedRegions = new TreeMap<>();
435    CatalogJanitor spy = spy(this.janitor);
436
437    CatalogJanitorReport report = new CatalogJanitorReport();
438    report.count = 10;
439    report.mergedRegions.putAll(mergedRegions);
440    report.splitParents.putAll(splitParents);
441
442    doReturn(report).when(spy).scanForReport();
443
444    // Create ref from splita to parent
445    LOG.info("parent=" + parent.getShortNameToLog() + ", splita=" + splita.getShortNameToLog());
446    Path splitaRef =
447      createReferences(this.masterServices, td, parent, splita, Bytes.toBytes("ccc"), false);
448    LOG.info("Created reference " + splitaRef);
449
450    // Parent and splita should not be removed because a reference from splita to parent.
451    int gcs = spy.scan();
452    assertEquals(0, gcs);
453
454    // Now delete the ref
455    FileSystem fs = FileSystem.get(HTU.getConfiguration());
456    assertTrue(fs.delete(splitaRef, true));
457
458    // now, both parent, and splita can be deleted
459    gcs = spy.scan();
460    assertEquals(2, gcs);
461  }
462
463  /**
464   * Test that we correctly archive all the storefiles when a region is deleted
465   */
466  @Test
467  public void testSplitParentFirstComparator() {
468    SplitParentFirstComparator comp = new SplitParentFirstComparator();
469    TableDescriptor td = createTableDescriptorForCurrentMethod();
470
471    /*
472     * Region splits: rootRegion --- firstRegion --- firstRegiona | |- firstRegionb | |- lastRegion
473     * --- lastRegiona --- lastRegionaa | |- lastRegionab |- lastRegionb rootRegion : [] - []
474     * firstRegion : [] - bbb lastRegion : bbb - [] firstRegiona : [] - aaa firstRegionb : aaa - bbb
475     * lastRegiona : bbb - ddd lastRegionb : ddd - []
476     */
477
478    // root region
479    RegionInfo rootRegion = createRegionInfo(td.getTableName(), HConstants.EMPTY_START_ROW,
480      HConstants.EMPTY_END_ROW, true);
481    RegionInfo firstRegion =
482      createRegionInfo(td.getTableName(), HConstants.EMPTY_START_ROW, Bytes.toBytes("bbb"), true);
483    RegionInfo lastRegion =
484      createRegionInfo(td.getTableName(), Bytes.toBytes("bbb"), HConstants.EMPTY_END_ROW, true);
485
486    assertTrue(comp.compare(rootRegion, rootRegion) == 0);
487    assertTrue(comp.compare(firstRegion, firstRegion) == 0);
488    assertTrue(comp.compare(lastRegion, lastRegion) == 0);
489    assertTrue(comp.compare(rootRegion, firstRegion) < 0);
490    assertTrue(comp.compare(rootRegion, lastRegion) < 0);
491    assertTrue(comp.compare(firstRegion, lastRegion) < 0);
492
493    // first region split into a, b
494    RegionInfo firstRegiona =
495      createRegionInfo(td.getTableName(), HConstants.EMPTY_START_ROW, Bytes.toBytes("aaa"), true);
496    RegionInfo firstRegionb =
497      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("bbb"), true);
498    // last region split into a, b
499    RegionInfo lastRegiona =
500      createRegionInfo(td.getTableName(), Bytes.toBytes("bbb"), Bytes.toBytes("ddd"), true);
501    RegionInfo lastRegionb =
502      createRegionInfo(td.getTableName(), Bytes.toBytes("ddd"), HConstants.EMPTY_END_ROW, true);
503
504    assertTrue(comp.compare(firstRegiona, firstRegiona) == 0);
505    assertTrue(comp.compare(firstRegionb, firstRegionb) == 0);
506    assertTrue(comp.compare(rootRegion, firstRegiona) < 0);
507    assertTrue(comp.compare(rootRegion, firstRegionb) < 0);
508    assertTrue(comp.compare(firstRegion, firstRegiona) < 0);
509    assertTrue(comp.compare(firstRegion, firstRegionb) < 0);
510    assertTrue(comp.compare(firstRegiona, firstRegionb) < 0);
511
512    assertTrue(comp.compare(lastRegiona, lastRegiona) == 0);
513    assertTrue(comp.compare(lastRegionb, lastRegionb) == 0);
514    assertTrue(comp.compare(rootRegion, lastRegiona) < 0);
515    assertTrue(comp.compare(rootRegion, lastRegionb) < 0);
516    assertTrue(comp.compare(lastRegion, lastRegiona) < 0);
517    assertTrue(comp.compare(lastRegion, lastRegionb) < 0);
518    assertTrue(comp.compare(lastRegiona, lastRegionb) < 0);
519
520    assertTrue(comp.compare(firstRegiona, lastRegiona) < 0);
521    assertTrue(comp.compare(firstRegiona, lastRegionb) < 0);
522    assertTrue(comp.compare(firstRegionb, lastRegiona) < 0);
523    assertTrue(comp.compare(firstRegionb, lastRegionb) < 0);
524
525    RegionInfo lastRegionaa =
526      createRegionInfo(td.getTableName(), Bytes.toBytes("bbb"), Bytes.toBytes("ccc"), false);
527    RegionInfo lastRegionab =
528      createRegionInfo(td.getTableName(), Bytes.toBytes("ccc"), Bytes.toBytes("ddd"), false);
529
530    assertTrue(comp.compare(lastRegiona, lastRegionaa) < 0);
531    assertTrue(comp.compare(lastRegiona, lastRegionab) < 0);
532    assertTrue(comp.compare(lastRegionaa, lastRegionab) < 0);
533  }
534
535  @Test
536  public void testArchiveOldRegion() throws Exception {
537    // Create regions.
538    TableDescriptor td = createTableDescriptorForCurrentMethod();
539    RegionInfo parent =
540      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("eee"));
541    RegionInfo splita =
542      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("ccc"));
543    RegionInfo splitb =
544      createRegionInfo(td.getTableName(), Bytes.toBytes("ccc"), Bytes.toBytes("eee"));
545
546    // Test that when both daughter regions are in place, that we do not
547    // remove the parent.
548    Result parentMetaRow = createResult(parent, splita, splitb);
549    FileSystem fs = FileSystem.get(HTU.getConfiguration());
550    Path rootdir = this.masterServices.getMasterFileSystem().getRootDir();
551    // have to set the root directory since we use it in HFileDisposer to figure out to get to the
552    // archive directory. Otherwise, it just seems to pick the first root directory it can find (so
553    // the single test passes, but when the full suite is run, things get borked).
554    CommonFSUtils.setRootDir(fs.getConf(), rootdir);
555    Path tabledir = CommonFSUtils.getTableDir(rootdir, td.getTableName());
556    Path storedir =
557      HRegionFileSystem.getStoreHomedir(tabledir, parent, td.getColumnFamilies()[0].getName());
558    Path storeArchive = HFileArchiveUtil.getStoreArchivePath(this.masterServices.getConfiguration(),
559      parent, tabledir, td.getColumnFamilies()[0].getName());
560    LOG.debug("Table dir:" + tabledir);
561    LOG.debug("Store dir:" + storedir);
562    LOG.debug("Store archive dir:" + storeArchive);
563
564    // add a couple of store files that we can check for
565    FileStatus[] mockFiles = addMockStoreFiles(2, this.masterServices, storedir);
566    // get the current store files for comparison
567    FileStatus[] storeFiles = fs.listStatus(storedir);
568    int index = 0;
569    for (FileStatus file : storeFiles) {
570      LOG.debug("Have store file:" + file.getPath());
571      assertEquals(mockFiles[index].getPath(), storeFiles[index].getPath(),
572        "Got unexpected store file");
573      index++;
574    }
575
576    // do the cleaning of the parent
577    assertTrue(CatalogJanitor.cleanParent(masterServices, parent, parentMetaRow));
578    Path parentDir = new Path(tabledir, parent.getEncodedName());
579    // Cleanup procedure runs async. Wait till it done.
580    ProcedureTestingUtility.waitAllProcedures(masterServices.getMasterProcedureExecutor());
581    assertTrue(!fs.exists(parentDir));
582    LOG.debug("Finished cleanup of parent region");
583
584    // and now check to make sure that the files have actually been archived
585    FileStatus[] archivedStoreFiles = fs.listStatus(storeArchive);
586    logFiles("archived files", storeFiles);
587    logFiles("archived files", archivedStoreFiles);
588
589    assertArchiveEqualToOriginal(storeFiles, archivedStoreFiles, fs);
590
591    // cleanup
592    CommonFSUtils.delete(fs, rootdir, true);
593  }
594
595  /**
596   * @param description description of the files for logging
597   * @param storeFiles  the status of the files to log
598   */
599  private void logFiles(String description, FileStatus[] storeFiles) {
600    LOG.debug("Current " + description + ": ");
601    for (FileStatus file : storeFiles) {
602      LOG.debug(Objects.toString(file.getPath()));
603    }
604  }
605
606  /**
607   * Test that if a store file with the same name is present as those already backed up cause the
608   * already archived files to be timestamped backup
609   */
610  @Test
611  public void testDuplicateHFileResolution() throws Exception {
612    TableDescriptor td = createTableDescriptorForCurrentMethod();
613
614    // Create regions.
615    RegionInfo parent =
616      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("eee"));
617    RegionInfo splita =
618      createRegionInfo(td.getTableName(), Bytes.toBytes("aaa"), Bytes.toBytes("ccc"));
619    RegionInfo splitb =
620      createRegionInfo(td.getTableName(), Bytes.toBytes("ccc"), Bytes.toBytes("eee"));
621    // Test that when both daughter regions are in place, that we do not
622    // remove the parent.
623    Result r = createResult(parent, splita, splitb);
624    FileSystem fs = FileSystem.get(HTU.getConfiguration());
625    Path rootdir = this.masterServices.getMasterFileSystem().getRootDir();
626    // Have to set the root directory since we use it in HFileDisposer to figure out to get to the
627    // archive directory. Otherwise, it just seems to pick the first root directory it can find (so
628    // the single test passes, but when the full suite is run, things get borked).
629    CommonFSUtils.setRootDir(fs.getConf(), rootdir);
630    Path tabledir = CommonFSUtils.getTableDir(rootdir, parent.getTable());
631    Path storedir =
632      HRegionFileSystem.getStoreHomedir(tabledir, parent, td.getColumnFamilies()[0].getName());
633    LOG.info("Old root:" + rootdir);
634    LOG.info("Old table:" + tabledir);
635    LOG.info("Old store:" + storedir);
636
637    Path storeArchive = HFileArchiveUtil.getStoreArchivePath(this.masterServices.getConfiguration(),
638      parent, tabledir, td.getColumnFamilies()[0].getName());
639    LOG.info("Old archive:" + storeArchive);
640
641    // enable archiving, make sure that files get archived
642    addMockStoreFiles(2, this.masterServices, storedir);
643    // get the current store files for comparison
644    FileStatus[] storeFiles = fs.listStatus(storedir);
645    // Do the cleaning of the parent
646    assertTrue(CatalogJanitor.cleanParent(masterServices, parent, r));
647    Path parentDir = new Path(tabledir, parent.getEncodedName());
648    ProcedureTestingUtility.waitAllProcedures(masterServices.getMasterProcedureExecutor());
649    assertTrue(!fs.exists(parentDir));
650
651    // And now check to make sure that the files have actually been archived
652    FileStatus[] archivedStoreFiles = fs.listStatus(storeArchive);
653    assertArchiveEqualToOriginal(storeFiles, archivedStoreFiles, fs);
654
655    // now add store files with the same names as before to check backup
656    // enable archiving, make sure that files get archived
657    addMockStoreFiles(2, this.masterServices, storedir);
658
659    // Do the cleaning of the parent
660    assertTrue(CatalogJanitor.cleanParent(masterServices, parent, r));
661    // Cleanup procedure runs async. Wait till it done.
662    ProcedureTestingUtility.waitAllProcedures(masterServices.getMasterProcedureExecutor());
663    assertTrue(!fs.exists(parentDir));
664
665    // and now check to make sure that the files have actually been archived
666    archivedStoreFiles = fs.listStatus(storeArchive);
667    assertArchiveEqualToOriginal(storeFiles, archivedStoreFiles, fs, true);
668  }
669
670  @Test
671  public void testAlreadyRunningStatus() throws Exception {
672    int numberOfThreads = 2;
673    List<Integer> gcValues = new ArrayList<>();
674    Thread[] threads = new Thread[numberOfThreads];
675    for (int i = 0; i < numberOfThreads; i++) {
676      threads[i] = new Thread(() -> {
677        try {
678          gcValues.add(janitor.scan());
679        } catch (IOException e) {
680          throw new RuntimeException(e);
681        }
682      });
683    }
684    for (int i = 0; i < numberOfThreads; i++) {
685      threads[i].start();
686    }
687    for (int i = 0; i < numberOfThreads; i++) {
688      threads[i].join();
689    }
690    assertTrue(gcValues.contains(-1), "One janitor.scan() call should have returned -1");
691  }
692
693  private FileStatus[] addMockStoreFiles(int count, MasterServices services, Path storedir)
694    throws IOException {
695    // get the existing store files
696    FileSystem fs = services.getMasterFileSystem().getFileSystem();
697    fs.mkdirs(storedir);
698    // create the store files in the parent
699    for (int i = 0; i < count; i++) {
700      Path storeFile = new Path(storedir, "_store" + i);
701      FSDataOutputStream dos = fs.create(storeFile, true);
702      dos.writeBytes("Some data: " + i);
703      dos.close();
704    }
705    LOG.debug("Adding " + count + " store files to the storedir:" + storedir);
706    // make sure the mock store files are there
707    FileStatus[] storeFiles = fs.listStatus(storedir);
708    assertEquals(count, storeFiles.length, "Didn't have expected store files");
709    return storeFiles;
710  }
711
712  private String setRootDirAndCleanIt(final HBaseTestingUtil htu, final String subdir)
713    throws IOException {
714    Path testdir = htu.getDataTestDir(subdir);
715    FileSystem fs = FileSystem.get(htu.getConfiguration());
716    if (fs.exists(testdir)) {
717      assertTrue(fs.delete(testdir, true));
718    }
719    CommonFSUtils.setRootDir(htu.getConfiguration(), testdir);
720    return CommonFSUtils.getRootDir(htu.getConfiguration()).toString();
721  }
722
723  private Path createReferences(final MasterServices services, final TableDescriptor td,
724    final RegionInfo parent, final RegionInfo daughter, final byte[] midkey, final boolean top)
725    throws IOException {
726    Path rootdir = services.getMasterFileSystem().getRootDir();
727    Path tabledir = CommonFSUtils.getTableDir(rootdir, parent.getTable());
728    Path storedir =
729      HRegionFileSystem.getStoreHomedir(tabledir, daughter, td.getColumnFamilies()[0].getName());
730    Reference ref =
731      top ? Reference.createTopReference(midkey) : Reference.createBottomReference(midkey);
732    long now = EnvironmentEdgeManager.currentTime();
733    // Reference name has this format: StoreFile#REF_NAME_PARSER
734    Path p = new Path(storedir, Long.toString(now) + "." + parent.getEncodedName());
735    FileSystem fs = services.getMasterFileSystem().getFileSystem();
736    HRegionFileSystem regionFS =
737      HRegionFileSystem.create(services.getConfiguration(), fs, tabledir, daughter);
738    StoreContext storeContext =
739      StoreContext.getBuilder().withColumnFamilyDescriptor(td.getColumnFamilies()[0])
740        .withFamilyStoreDirectoryPath(storedir).withRegionFileSystem(regionFS).build();
741    StoreFileTracker sft =
742      StoreFileTrackerFactory.create(services.getConfiguration(), false, storeContext);
743    sft.createReference(ref, p);
744    return p;
745  }
746
747  private Result createResult(final RegionInfo parent, final RegionInfo a, final RegionInfo b)
748    throws IOException {
749    return MetaMockingUtil.getMetaTableRowResult(parent, null, a, b);
750  }
751}