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}