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.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertTrue;
022
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.List;
026import org.apache.hadoop.conf.Configuration;
027import org.apache.hadoop.fs.FileSystem;
028import org.apache.hadoop.fs.Path;
029import org.apache.hadoop.hbase.HBaseTestingUtil;
030import org.apache.hadoop.hbase.HConstants;
031import org.apache.hadoop.hbase.TableName;
032import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
033import org.apache.hadoop.hbase.master.snapshot.SnapshotManager;
034import org.apache.hadoop.hbase.regionserver.BloomType;
035import org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy;
036import org.apache.hadoop.hbase.snapshot.SnapshotTestingUtils;
037import org.apache.hadoop.hbase.testclassification.ClientTests;
038import org.apache.hadoop.hbase.testclassification.MediumTests;
039import org.apache.hadoop.hbase.util.Bytes;
040import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
041import org.junit.jupiter.api.AfterAll;
042import org.junit.jupiter.api.AfterEach;
043import org.junit.jupiter.api.BeforeAll;
044import org.junit.jupiter.api.BeforeEach;
045import org.junit.jupiter.api.Tag;
046import org.junit.jupiter.api.Test;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050/**
051 * Test class to verify that metadata is consistent before and after a snapshot attempt.
052 */
053@Tag(MediumTests.TAG)
054@Tag(ClientTests.TAG)
055public class TestSnapshotMetadata {
056
057  private static final Logger LOG = LoggerFactory.getLogger(TestSnapshotMetadata.class);
058
059  private static final HBaseTestingUtil UTIL = new HBaseTestingUtil();
060  private static final int NUM_RS = 2;
061  private static final String STRING_TABLE_NAME = "TestSnapshotMetadata";
062
063  private static final String MAX_VERSIONS_FAM_STR = "fam_max_columns";
064  private static final byte[] MAX_VERSIONS_FAM = Bytes.toBytes(MAX_VERSIONS_FAM_STR);
065
066  private static final String COMPRESSED_FAM_STR = "fam_compressed";
067  private static final byte[] COMPRESSED_FAM = Bytes.toBytes(COMPRESSED_FAM_STR);
068
069  private static final String BLOCKSIZE_FAM_STR = "fam_blocksize";
070  private static final byte[] BLOCKSIZE_FAM = Bytes.toBytes(BLOCKSIZE_FAM_STR);
071
072  private static final String BLOOMFILTER_FAM_STR = "fam_bloomfilter";
073  private static final byte[] BLOOMFILTER_FAM = Bytes.toBytes(BLOOMFILTER_FAM_STR);
074
075  private static final String TEST_CONF_CUSTOM_VALUE = "TestCustomConf";
076  private static final String TEST_CUSTOM_VALUE = "TestCustomValue";
077
078  private static final byte[][] families =
079    { MAX_VERSIONS_FAM, BLOOMFILTER_FAM, COMPRESSED_FAM, BLOCKSIZE_FAM };
080
081  private static final DataBlockEncoding DATA_BLOCK_ENCODING_TYPE = DataBlockEncoding.FAST_DIFF;
082  private static final BloomType BLOOM_TYPE = BloomType.ROW;
083  private static final int BLOCK_SIZE = 98;
084  private static final int MAX_VERSIONS = 8;
085
086  private Admin admin;
087  private String originalTableDescription;
088  private TableDescriptor originalTableDescriptor;
089  TableName originalTableName;
090
091  private static FileSystem fs;
092  private static Path rootDir;
093
094  @BeforeAll
095  public static void setupCluster() throws Exception {
096    setupConf(UTIL.getConfiguration());
097    UTIL.startMiniCluster(NUM_RS);
098
099    fs = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getFileSystem();
100    rootDir = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir();
101  }
102
103  @AfterAll
104  public static void cleanupTest() throws Exception {
105    try {
106      UTIL.shutdownMiniCluster();
107    } catch (Exception e) {
108      LOG.warn("failure shutting down cluster", e);
109    }
110  }
111
112  private static void setupConf(Configuration conf) {
113    // enable snapshot support
114    conf.setBoolean(SnapshotManager.HBASE_SNAPSHOT_ENABLED, true);
115    // disable the ui
116    conf.setInt("hbase.regionsever.info.port", -1);
117    // change the flush size to a small amount, regulating number of store files
118    conf.setInt("hbase.hregion.memstore.flush.size", 25000);
119    // so make sure we get a compaction when doing a load, but keep around
120    // some files in the store
121    conf.setInt("hbase.hstore.compaction.min", 10);
122    conf.setInt("hbase.hstore.compactionThreshold", 10);
123    // block writes if we get to 12 store files
124    conf.setInt("hbase.hstore.blockingStoreFiles", 12);
125    conf.setInt("hbase.regionserver.msginterval", 100);
126    conf.setBoolean("hbase.master.enabletable.roundrobin", true);
127    // Avoid potentially aggressive splitting which would cause snapshot to fail
128    conf.set(HConstants.HBASE_REGION_SPLIT_POLICY_KEY,
129      ConstantSizeRegionSplitPolicy.class.getName());
130  }
131
132  @BeforeEach
133  public void setup() throws Exception {
134    admin = UTIL.getAdmin();
135    createTableWithNonDefaultProperties();
136  }
137
138  @AfterEach
139  public void tearDown() throws Exception {
140    SnapshotTestingUtils.deleteAllSnapshots(admin);
141  }
142
143  /*
144   * Create a table that has non-default properties so we can see if they hold
145   */
146  private void createTableWithNonDefaultProperties() throws Exception {
147    final long startTime = EnvironmentEdgeManager.currentTime();
148    final String sourceTableNameAsString = STRING_TABLE_NAME + startTime;
149    originalTableName = TableName.valueOf(sourceTableNameAsString);
150
151    // enable replication on a column family
152    ColumnFamilyDescriptor maxVersionsColumn = ColumnFamilyDescriptorBuilder
153      .newBuilder(MAX_VERSIONS_FAM).setMaxVersions(MAX_VERSIONS).build();
154    ColumnFamilyDescriptor bloomFilterColumn = ColumnFamilyDescriptorBuilder
155      .newBuilder(BLOOMFILTER_FAM).setBloomFilterType(BLOOM_TYPE).build();
156    ColumnFamilyDescriptor dataBlockColumn = ColumnFamilyDescriptorBuilder
157      .newBuilder(COMPRESSED_FAM).setDataBlockEncoding(DATA_BLOCK_ENCODING_TYPE).build();
158    ColumnFamilyDescriptor blockSizeColumn =
159      ColumnFamilyDescriptorBuilder.newBuilder(BLOCKSIZE_FAM).setBlocksize(BLOCK_SIZE).build();
160
161    TableDescriptor tableDescriptor = TableDescriptorBuilder
162      .newBuilder(TableName.valueOf(sourceTableNameAsString)).setColumnFamily(maxVersionsColumn)
163      .setColumnFamily(bloomFilterColumn).setColumnFamily(dataBlockColumn)
164      .setColumnFamily(blockSizeColumn).setValue(TEST_CUSTOM_VALUE, TEST_CUSTOM_VALUE)
165      .setValue(TEST_CONF_CUSTOM_VALUE, TEST_CONF_CUSTOM_VALUE).build();
166    assertTrue(tableDescriptor.getValues().size() > 0);
167
168    admin.createTable(tableDescriptor);
169    Table original = UTIL.getConnection().getTable(originalTableName);
170    originalTableName = TableName.valueOf(sourceTableNameAsString);
171    originalTableDescriptor = admin.getDescriptor(originalTableName);
172    originalTableDescription = originalTableDescriptor.toStringCustomizedValues();
173
174    original.close();
175  }
176
177  /**
178   * Verify that the describe for a cloned table matches the describe from the original.
179   */
180  @Test
181  public void testDescribeMatchesAfterClone() throws Exception {
182    // Clone the original table
183    final String clonedTableNameAsString = "clone" + originalTableName;
184    final TableName clonedTableName = TableName.valueOf(clonedTableNameAsString);
185    final String snapshotNameAsString =
186      "snapshot" + originalTableName + EnvironmentEdgeManager.currentTime();
187    final String snapshotName = snapshotNameAsString;
188
189    // restore the snapshot into a cloned table and examine the output
190    List<byte[]> familiesList = new ArrayList<>();
191    Collections.addAll(familiesList, families);
192
193    // Create a snapshot in which all families are empty
194    SnapshotTestingUtils.createSnapshotAndValidate(admin, originalTableName, null, familiesList,
195      snapshotNameAsString, rootDir, fs, /* onlineSnapshot= */ false);
196
197    admin.cloneSnapshot(snapshotName, clonedTableName);
198    Table clonedTable = UTIL.getConnection().getTable(clonedTableName);
199    TableDescriptor cloneHtd = admin.getDescriptor(clonedTableName);
200    assertEquals(originalTableDescription.replace(originalTableName.getNameAsString(),
201      clonedTableNameAsString), cloneHtd.toStringCustomizedValues());
202
203    // Verify the custom fields
204    assertEquals(originalTableDescriptor.getValues().size(), cloneHtd.getValues().size());
205    assertEquals(TEST_CUSTOM_VALUE, cloneHtd.getValue(TEST_CUSTOM_VALUE));
206    assertEquals(TEST_CONF_CUSTOM_VALUE, cloneHtd.getValue(TEST_CONF_CUSTOM_VALUE));
207    assertEquals(originalTableDescriptor.getValues(), cloneHtd.getValues());
208
209    admin.enableTable(originalTableName);
210    clonedTable.close();
211  }
212
213  /**
214   * Verify that the describe for a restored table matches the describe for one the original.
215   */
216  @Test
217  public void testDescribeMatchesAfterRestore() throws Exception {
218    runRestoreWithAdditionalMetadata(false);
219  }
220
221  /**
222   * Verify that if metadata changed after a snapshot was taken, that the old metadata replaces the
223   * new metadata during a restore
224   */
225  @Test
226  public void testDescribeMatchesAfterMetadataChangeAndRestore() throws Exception {
227    runRestoreWithAdditionalMetadata(true);
228  }
229
230  /**
231   * Verify that when the table is empty, making metadata changes after the restore does not affect
232   * the restored table's original metadata
233   */
234  @Test
235  public void testDescribeOnEmptyTableMatchesAfterMetadataChangeAndRestore() throws Exception {
236    runRestoreWithAdditionalMetadata(true, false);
237  }
238
239  private void runRestoreWithAdditionalMetadata(boolean changeMetadata) throws Exception {
240    runRestoreWithAdditionalMetadata(changeMetadata, true);
241  }
242
243  private void runRestoreWithAdditionalMetadata(boolean changeMetadata, boolean addData)
244    throws Exception {
245
246    if (admin.isTableDisabled(originalTableName)) {
247      admin.enableTable(originalTableName);
248    }
249
250    // populate it with data
251    final byte[] familyForUpdate = BLOCKSIZE_FAM;
252
253    List<byte[]> familiesWithDataList = new ArrayList<>();
254    List<byte[]> emptyFamiliesList = new ArrayList<>();
255    if (addData) {
256      Table original = UTIL.getConnection().getTable(originalTableName);
257      UTIL.loadTable(original, familyForUpdate); // family arbitrarily chosen
258      original.close();
259
260      for (byte[] family : families) {
261        if (family != familyForUpdate) {
262          emptyFamiliesList.add(family);
263        }
264      }
265      familiesWithDataList.add(familyForUpdate);
266    } else {
267      Collections.addAll(emptyFamiliesList, families);
268    }
269
270    // take a "disabled" snapshot
271    final String snapshotNameAsString =
272      "snapshot" + originalTableName + EnvironmentEdgeManager.currentTime();
273
274    SnapshotTestingUtils.createSnapshotAndValidate(admin, originalTableName, familiesWithDataList,
275      emptyFamiliesList, snapshotNameAsString, rootDir, fs, /* onlineSnapshot= */ false);
276
277    admin.enableTable(originalTableName);
278
279    if (changeMetadata) {
280      final String newFamilyNameAsString = "newFamily" + EnvironmentEdgeManager.currentTime();
281      final byte[] newFamilyName = Bytes.toBytes(newFamilyNameAsString);
282
283      admin.disableTable(originalTableName);
284      ColumnFamilyDescriptor familyDescriptor = ColumnFamilyDescriptorBuilder.of(newFamilyName);
285      admin.addColumnFamily(originalTableName, familyDescriptor);
286      assertTrue(admin.getDescriptor(originalTableName).toString().contains(newFamilyNameAsString),
287        "New column family was not added.");
288    }
289
290    // restore it
291    if (!admin.isTableDisabled(originalTableName)) {
292      admin.disableTable(originalTableName);
293    }
294
295    admin.restoreSnapshot(snapshotNameAsString);
296    admin.enableTable(originalTableName);
297
298    // verify that the descrption is reverted
299    try (Table original = UTIL.getConnection().getTable(originalTableName)) {
300      assertEquals(originalTableDescriptor, admin.getDescriptor(originalTableName));
301      assertEquals(originalTableDescriptor, original.getDescriptor());
302    }
303  }
304}