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(30)).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 // admin.getRegions() can report the daughters (from meta) before the region servers have opened 129 // them and loaded their store files, so retry until the links are actually online. 130 await().atMost(Duration.ofSeconds(30)).untilAsserted(this::assertDaughtersHaveOnlyLinks); 131 132 // Take a snapshot and clone it. 133 admin.snapshot(snapshotName, tableName); 134 admin.cloneSnapshot(snapshotName, clonedTableName); 135 SnapshotTestingUtils.waitForTableToBeOnline(TEST_UTIL, clonedTableName); 136 137 // The cloned table must contain all rows. 138 verifyRowCount(TEST_UTIL, clonedTableName, totalRows); 139 140 // Guard: because the daughters were cloned from whole-file HFileLinks (not References), the 141 // cloned table's meta does not record the parent's split daughters (SPLITA/SPLITB). The 142 // daughters link directly to the snapshot files and do not depend on the cloned parent. 143 assertClonedSplitParentHasNoDaughters(); 144 145 // Remove the source table and the snapshot, then run the HFile cleaner. The cloned table's 146 // HFileLinks (and their back-references) must keep the underlying files alive in the archive. 147 TEST_UTIL.deleteTable(tableName); 148 admin.deleteSnapshot(snapshotName); 149 runHFileCleaner(); 150 151 // Reopen the cloned table to force the store files to be re-resolved from disk, then verify 152 // that no data was lost. 153 admin.disableTable(clonedTableName); 154 admin.enableTable(clonedTableName); 155 SnapshotTestingUtils.waitForTableToBeOnline(TEST_UTIL, clonedTableName); 156 verifyRowCount(TEST_UTIL, clonedTableName, totalRows); 157 } 158 159 private int loadTwoDisjointStoreFiles() throws IOException { 160 try (Table table = TEST_UTIL.getConnection().getTable(tableName)) { 161 putBatch(table, LOW_PREFIX); 162 TEST_UTIL.flush(tableName); 163 putBatch(table, HIGH_PREFIX); 164 TEST_UTIL.flush(tableName); 165 return countRows(table); 166 } 167 } 168 169 private void putBatch(Table table, String prefix) throws IOException { 170 List<Put> puts = new ArrayList<>(); 171 for (int i = 0; i < ROWS_PER_BATCH; i++) { 172 Put put = new Put(Bytes.toBytes(prefix + String.format("%03d", i))); 173 put.addColumn(FAMILY, QUALIFIER, Bytes.toBytes(prefix + i)); 174 puts.add(put); 175 } 176 table.put(puts); 177 } 178 179 private void assertDaughtersHaveOnlyLinks() throws IOException { 180 int linkFiles = 0; 181 for (HRegion region : TEST_UTIL.getHBaseCluster().getRegions(tableName)) { 182 if ( 183 !RegionReplicaUtil.isDefaultReplica(region.getRegionInfo()) 184 || region.getRegionInfo().isSplitParent() 185 ) { 186 continue; 187 } 188 for (HStore store : region.getStores()) { 189 for (HStoreFile sf : store.getStorefiles()) { 190 assertTrue(sf.getFileInfo().isLink(), 191 "Expected only whole-file HFileLinks after the split, but found a non-link store file: " 192 + sf.getPath()); 193 linkFiles++; 194 } 195 } 196 } 197 assertTrue(linkFiles >= 2, 198 "Expected at least two HFileLink files across the daughter regions, found " + linkFiles); 199 } 200 201 private void assertClonedSplitParentHasNoDaughters() throws IOException { 202 RegionStates regionStates = 203 TEST_UTIL.getHBaseCluster().getMaster().getAssignmentManager().getRegionStates(); 204 List<RegionInfo> splitParents = 205 regionStates.getRegionByStateOfTable(clonedTableName).get(RegionState.State.SPLIT); 206 assertNotNull(splitParents); 207 assertFalse(splitParents.isEmpty(), "The cloned table should contain a split parent region"); 208 for (RegionInfo splitParent : splitParents) { 209 Result result = MetaTableAccessor.getRegionResult(TEST_UTIL.getConnection(), splitParent); 210 PairOfSameType<RegionInfo> daughters = MetaTableAccessor.getDaughterRegions(result); 211 assertNull(daughters.getFirst(), 212 "Did not expect SPLITA to be recorded for an all-HFileLink clone"); 213 assertNull(daughters.getSecond(), 214 "Did not expect SPLITB to be recorded for an all-HFileLink clone"); 215 } 216 } 217 218 private void runHFileCleaner() throws IOException { 219 TEST_UTIL.getMiniHBaseCluster().getMaster().getHFileCleaner().choreForTesting(); 220 } 221}