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.client;
019
020import static org.awaitility.Awaitility.await;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertFalse;
023import static org.junit.jupiter.api.Assertions.assertNotNull;
024import static org.junit.jupiter.api.Assertions.assertNull;
025import static org.junit.jupiter.api.Assertions.assertTrue;
026
027import java.io.IOException;
028import java.time.Duration;
029import java.util.ArrayList;
030import java.util.List;
031import org.apache.hadoop.hbase.MetaTableAccessor;
032import org.apache.hadoop.hbase.TableName;
033import org.apache.hadoop.hbase.master.RegionState;
034import org.apache.hadoop.hbase.master.assignment.RegionStates;
035import org.apache.hadoop.hbase.master.cleaner.TimeToLiveHFileCleaner;
036import org.apache.hadoop.hbase.regionserver.HRegion;
037import org.apache.hadoop.hbase.regionserver.HStore;
038import org.apache.hadoop.hbase.regionserver.HStoreFile;
039import org.apache.hadoop.hbase.snapshot.SnapshotTestingUtils;
040import org.apache.hadoop.hbase.util.Bytes;
041import org.apache.hadoop.hbase.util.PairOfSameType;
042import org.junit.jupiter.api.AfterEach;
043import org.junit.jupiter.api.TestTemplate;
044
045/**
046 * Base class for testing the clone-snapshot flow when the snapshot was taken after a split that
047 * produced whole-file {@code HFileLink}s for the daughters instead of {@code Reference} files.
048 * <p>
049 * Since HBASE-26421, a split builds an {@code HFileLink} (not a {@code Reference}) for a daughter
050 * whenever a store file lies entirely on one side of the split point. When every store file falls
051 * on one side, the snapshot contains no reference files at all, so the daughters link directly to
052 * the snapshot files and do not depend on the cloned parent region. This is the complement of the
053 * {@code Reference} case verified by {@code CloneSnapshotFromClientAfterSplittingRegionTestBase}
054 * (the regression HBASE-29111 guards against): here there is no parent-to-daughter mapping to
055 * record, and the cloned table must still be safe after the source table and snapshot are removed.
056 */
057public class CloneSnapshotFromClientAfterSplittingRegionWithLinksTestBase
058  extends CloneSnapshotFromClientTestBase {
059
060  private static final byte[] QUALIFIER = Bytes.toBytes("q");
061
062  private static final int ROWS_PER_BATCH = 10;
063
064  // Two disjoint key ranges and a split point strictly between them. After splitting, the "low"
065  // store file lies entirely below SPLIT_KEY and the "high" store file entirely above it, so each
066  // daughter receives one whole store file as an HFileLink and neither side gets a Reference.
067  private static final String LOW_PREFIX = "a";
068  private static final String HIGH_PREFIX = "z";
069  private static final byte[] SPLIT_KEY = Bytes.toBytes("m");
070
071  private TableName clonedTableName;
072  private String snapshotName;
073
074  protected CloneSnapshotFromClientAfterSplittingRegionWithLinksTestBase(int numReplicas) {
075    super(numReplicas);
076  }
077
078  protected static void setupConfiguration() {
079    CloneSnapshotFromClientTestBase.setupConfiguration();
080    // CloneSnapshotFromClientTestBase already disables compaction, which keeps the two store files
081    // we create from being merged into one that would straddle the split point. On top of that,
082    // make archived files immediately eligible for cleaning so that the data-survival check below
083    // is meaningful: only the cloned table's HFileLink back-references should keep them alive.
084    TEST_UTIL.getConfiguration().setLong(TimeToLiveHFileCleaner.TTL_CONF_KEY, 0);
085  }
086
087  @Override
088  protected void initSnapshotNames(long tid) {
089    clonedTableName = TableName.valueOf(getValidMethodName() + "-clone-" + tid);
090    snapshotName = "snaptb-links-" + tid;
091  }
092
093  @Override
094  protected void createTableAndSnapshots() throws Exception {
095    createTable();
096    admin.catalogJanitorSwitch(false);
097  }
098
099  @AfterEach
100  public void tearDownClone() throws Exception {
101    admin.catalogJanitorSwitch(true);
102    if (clonedTableName != null && admin.tableExists(clonedTableName)) {
103      TEST_UTIL.deleteTable(clonedTableName);
104    }
105  }
106
107  /**
108   * Create a single-region table so that we fully control its store files before splitting.
109   */
110  @Override
111  protected void createTable() throws IOException, InterruptedException {
112    SnapshotTestingUtils.createTable(TEST_UTIL, tableName, numReplicas, 1, FAMILY);
113  }
114
115  @TestTemplate
116  public void testClonedTableWithLinksSurvivesSourceDeletion() throws Exception {
117    // Write two store files whose key ranges are disjoint and sit on opposite sides of SPLIT_KEY.
118    int totalRows = loadTwoDisjointStoreFiles();
119
120    // Split between the two ranges. Each daughter receives one whole store file as an HFileLink,
121    // so the snapshot contains only HFileLinks and no Reference files.
122    int numRegions = admin.getRegions(tableName).size();
123    admin.split(tableName, SPLIT_KEY);
124    await().atMost(Duration.ofSeconds(60)).untilAsserted(
125      () -> assertEquals(numRegions + numReplicas, admin.getRegions(tableName).size()));
126
127    // Guard: the split must have produced only HFileLinks (no Reference files) for the daughters,
128    // otherwise this test would silently degrade into the Reference case.
129    assertDaughtersHaveOnlyLinks();
130
131    // Take a snapshot and clone it.
132    admin.snapshot(snapshotName, tableName);
133    admin.cloneSnapshot(snapshotName, clonedTableName);
134    SnapshotTestingUtils.waitForTableToBeOnline(TEST_UTIL, clonedTableName);
135
136    // The cloned table must contain all rows.
137    verifyRowCount(TEST_UTIL, clonedTableName, totalRows);
138
139    // Guard: because the daughters were cloned from whole-file HFileLinks (not References), the
140    // cloned table's meta does not record the parent's split daughters (SPLITA/SPLITB). The
141    // daughters link directly to the snapshot files and do not depend on the cloned parent.
142    assertClonedSplitParentHasNoDaughters();
143
144    // Remove the source table and the snapshot, then run the HFile cleaner. The cloned table's
145    // HFileLinks (and their back-references) must keep the underlying files alive in the archive.
146    TEST_UTIL.deleteTable(tableName);
147    admin.deleteSnapshot(snapshotName);
148    runHFileCleaner();
149
150    // Reopen the cloned table to force the store files to be re-resolved from disk, then verify
151    // that no data was lost.
152    admin.disableTable(clonedTableName);
153    admin.enableTable(clonedTableName);
154    SnapshotTestingUtils.waitForTableToBeOnline(TEST_UTIL, clonedTableName);
155    verifyRowCount(TEST_UTIL, clonedTableName, totalRows);
156  }
157
158  private int loadTwoDisjointStoreFiles() throws IOException {
159    try (Table table = TEST_UTIL.getConnection().getTable(tableName)) {
160      putBatch(table, LOW_PREFIX);
161      TEST_UTIL.flush(tableName);
162      putBatch(table, HIGH_PREFIX);
163      TEST_UTIL.flush(tableName);
164      return countRows(table);
165    }
166  }
167
168  private void putBatch(Table table, String prefix) throws IOException {
169    List<Put> puts = new ArrayList<>();
170    for (int i = 0; i < ROWS_PER_BATCH; i++) {
171      Put put = new Put(Bytes.toBytes(prefix + String.format("%03d", i)));
172      put.addColumn(FAMILY, QUALIFIER, Bytes.toBytes(prefix + i));
173      puts.add(put);
174    }
175    table.put(puts);
176  }
177
178  private void assertDaughtersHaveOnlyLinks() throws IOException {
179    int linkFiles = 0;
180    for (HRegion region : TEST_UTIL.getHBaseCluster().getRegions(tableName)) {
181      if (
182        !RegionReplicaUtil.isDefaultReplica(region.getRegionInfo())
183          || region.getRegionInfo().isSplitParent()
184      ) {
185        continue;
186      }
187      for (HStore store : region.getStores()) {
188        for (HStoreFile sf : store.getStorefiles()) {
189          assertTrue(sf.getFileInfo().isLink(),
190            "Expected only whole-file HFileLinks after the split, but found a non-link store file: "
191              + sf.getPath());
192          linkFiles++;
193        }
194      }
195    }
196    assertTrue(linkFiles >= 2,
197      "Expected at least two HFileLink files across the daughter regions, found " + linkFiles);
198  }
199
200  private void assertClonedSplitParentHasNoDaughters() throws IOException {
201    RegionStates regionStates =
202      TEST_UTIL.getHBaseCluster().getMaster().getAssignmentManager().getRegionStates();
203    List<RegionInfo> splitParents =
204      regionStates.getRegionByStateOfTable(clonedTableName).get(RegionState.State.SPLIT);
205    assertNotNull(splitParents);
206    assertFalse(splitParents.isEmpty(), "The cloned table should contain a split parent region");
207    for (RegionInfo splitParent : splitParents) {
208      Result result = MetaTableAccessor.getRegionResult(TEST_UTIL.getConnection(), splitParent);
209      PairOfSameType<RegionInfo> daughters = MetaTableAccessor.getDaughterRegions(result);
210      assertNull(daughters.getFirst(),
211        "Did not expect SPLITA to be recorded for an all-HFileLink clone");
212      assertNull(daughters.getSecond(),
213        "Did not expect SPLITB to be recorded for an all-HFileLink clone");
214    }
215  }
216
217  private void runHFileCleaner() throws IOException {
218    TEST_UTIL.getMiniHBaseCluster().getMaster().getHFileCleaner().choreForTesting();
219  }
220}