001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to you under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.hadoop.hbase.quotas;
018
019import static org.junit.Assert.assertEquals;
020import static org.junit.Assert.assertNotNull;
021import static org.junit.Assert.assertTrue;
022
023import java.io.IOException;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map.Entry;
030import java.util.Set;
031import java.util.concurrent.atomic.AtomicLong;
032
033import org.apache.hadoop.conf.Configuration;
034import org.apache.hadoop.fs.FileSystem;
035import org.apache.hadoop.fs.Path;
036import org.apache.hadoop.hbase.Cell;
037import org.apache.hadoop.hbase.CellScanner;
038import org.apache.hadoop.hbase.HBaseClassTestRule;
039import org.apache.hadoop.hbase.HBaseTestingUtility;
040import org.apache.hadoop.hbase.TableName;
041import org.apache.hadoop.hbase.client.Admin;
042import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
043import org.apache.hadoop.hbase.client.Connection;
044import org.apache.hadoop.hbase.client.Get;
045import org.apache.hadoop.hbase.client.Result;
046import org.apache.hadoop.hbase.client.ResultScanner;
047import org.apache.hadoop.hbase.client.Scan;
048import org.apache.hadoop.hbase.client.SnapshotDescription;
049import org.apache.hadoop.hbase.client.SnapshotType;
050import org.apache.hadoop.hbase.client.Table;
051import org.apache.hadoop.hbase.client.TableDescriptor;
052import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
053import org.apache.hadoop.hbase.quotas.FileArchiverNotifierImpl.SnapshotWithSize;
054import org.apache.hadoop.hbase.snapshot.SnapshotDescriptionUtils;
055import org.apache.hadoop.hbase.snapshot.SnapshotManifest;
056import org.apache.hadoop.hbase.testclassification.MediumTests;
057import org.apache.hadoop.hbase.util.FSUtils;
058import org.junit.AfterClass;
059import org.junit.Before;
060import org.junit.BeforeClass;
061import org.junit.ClassRule;
062import org.junit.Rule;
063import org.junit.Test;
064import org.junit.experimental.categories.Category;
065import org.junit.rules.TestName;
066
067import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableSet;
068import org.apache.hbase.thirdparty.com.google.common.collect.Iterables;
069import org.apache.hbase.thirdparty.com.google.common.collect.Maps;
070
071import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos;
072import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest;
073import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest.FamilyFiles;
074import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest.StoreFile;
075
076/**
077 * Test class for {@link FileArchiverNotifierImpl}.
078 */
079@Category(MediumTests.class)
080public class TestFileArchiverNotifierImpl {
081  @ClassRule
082  public static final HBaseClassTestRule CLASS_RULE =
083      HBaseClassTestRule.forClass(TestFileArchiverNotifierImpl.class);
084
085  private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
086  private static final AtomicLong COUNTER = new AtomicLong();
087
088  @Rule
089  public TestName testName = new TestName();
090
091  private Connection conn;
092  private Admin admin;
093  private SpaceQuotaHelperForTests helper;
094  private FileSystem fs;
095  private Configuration conf;
096
097  @BeforeClass
098  public static void setUp() throws Exception {
099    Configuration conf = TEST_UTIL.getConfiguration();
100    SpaceQuotaHelperForTests.updateConfigForQuotas(conf);
101    // Clean up the compacted files faster than normal (15s instead of 2mins)
102    conf.setInt("hbase.hfile.compaction.discharger.interval", 15 * 1000);
103    // Prevent the SnapshotQuotaObserverChore from running
104    conf.setInt(SnapshotQuotaObserverChore.SNAPSHOT_QUOTA_CHORE_DELAY_KEY, 60 * 60 * 1000);
105    conf.setInt(SnapshotQuotaObserverChore.SNAPSHOT_QUOTA_CHORE_PERIOD_KEY, 60 * 60 * 1000);
106    TEST_UTIL.startMiniCluster(1);
107  }
108
109  @AfterClass
110  public static void tearDown() throws Exception {
111    TEST_UTIL.shutdownMiniCluster();
112  }
113
114  @Before
115  public void setup() throws Exception {
116    conn = TEST_UTIL.getConnection();
117    admin = TEST_UTIL.getAdmin();
118    helper = new SpaceQuotaHelperForTests(TEST_UTIL, testName, COUNTER);
119    helper.removeAllQuotas(conn);
120    fs = TEST_UTIL.getTestFileSystem();
121    conf = TEST_UTIL.getConfiguration();
122  }
123
124  @Test
125  public void testSnapshotSizePersistence() throws IOException {
126    final Admin admin = TEST_UTIL.getAdmin();
127    final TableName tn = TableName.valueOf(testName.getMethodName());
128    if (admin.tableExists(tn)) {
129      admin.disableTable(tn);
130      admin.deleteTable(tn);
131    }
132    TableDescriptor desc = TableDescriptorBuilder.newBuilder(tn).addColumnFamily(
133        ColumnFamilyDescriptorBuilder.of(QuotaTableUtil.QUOTA_FAMILY_USAGE)).build();
134    admin.createTable(desc);
135
136    FileArchiverNotifierImpl notifier = new FileArchiverNotifierImpl(conn, conf, fs, tn);
137    List<SnapshotWithSize> snapshotsWithSizes = new ArrayList<>();
138    try (Table table = conn.getTable(tn)) {
139      // Writing no values will result in no records written.
140      verify(table, () -> {
141        notifier.persistSnapshotSizes(table, snapshotsWithSizes);
142        assertEquals(0, count(table));
143      });
144
145      verify(table, () -> {
146        snapshotsWithSizes.add(new SnapshotWithSize("ss1", 1024L));
147        snapshotsWithSizes.add(new SnapshotWithSize("ss2", 4096L));
148        notifier.persistSnapshotSizes(table, snapshotsWithSizes);
149        assertEquals(2, count(table));
150        assertEquals(1024L, extractSnapshotSize(table, tn, "ss1"));
151        assertEquals(4096L, extractSnapshotSize(table, tn, "ss2"));
152      });
153    }
154  }
155
156  @Test
157  public void testIncrementalFileArchiving() throws Exception {
158    final Admin admin = TEST_UTIL.getAdmin();
159    final TableName tn = TableName.valueOf(testName.getMethodName());
160    if (admin.tableExists(tn)) {
161      admin.disableTable(tn);
162      admin.deleteTable(tn);
163    }
164    final Table quotaTable = conn.getTable(QuotaUtil.QUOTA_TABLE_NAME);
165    final TableName tn1 = helper.createTableWithRegions(1);
166    admin.setQuota(QuotaSettingsFactory.limitTableSpace(
167        tn1, SpaceQuotaHelperForTests.ONE_GIGABYTE, SpaceViolationPolicy.NO_INSERTS));
168
169    // Write some data and flush it
170    helper.writeData(tn1, 256L * SpaceQuotaHelperForTests.ONE_KILOBYTE);
171    admin.flush(tn1);
172
173    // Create a snapshot on the table
174    final String snapshotName1 = tn1 + "snapshot1";
175    admin.snapshot(new SnapshotDescription(snapshotName1, tn1, SnapshotType.SKIPFLUSH));
176
177    FileArchiverNotifierImpl notifier = new FileArchiverNotifierImpl(conn, conf, fs, tn);
178    long t1 = notifier.getLastFullCompute();
179    long snapshotSize = notifier.computeAndStoreSnapshotSizes(Arrays.asList(snapshotName1));
180    assertEquals("The size of the snapshots should be zero", 0, snapshotSize);
181    assertTrue("Last compute time was not less than current compute time",
182        t1 < notifier.getLastFullCompute());
183
184    // No recently archived files and the snapshot should have no size
185    assertEquals(0, extractSnapshotSize(quotaTable, tn, snapshotName1));
186
187    // Invoke the addArchivedFiles method with no files
188    notifier.addArchivedFiles(Collections.emptySet());
189
190    // The size should not have changed
191    assertEquals(0, extractSnapshotSize(quotaTable, tn, snapshotName1));
192
193    notifier.addArchivedFiles(ImmutableSet.of(entry("a", 1024L), entry("b", 1024L)));
194
195    // The size should not have changed
196    assertEquals(0, extractSnapshotSize(quotaTable, tn, snapshotName1));
197
198    // Pull one file referenced by the snapshot out of the manifest
199    Set<String> referencedFiles = getFilesReferencedBySnapshot(snapshotName1);
200    assertTrue("Found snapshot referenced files: " + referencedFiles, referencedFiles.size() >= 1);
201    String referencedFile = Iterables.getFirst(referencedFiles, null);
202    assertNotNull(referencedFile);
203
204    // Report that a file this snapshot referenced was moved to the archive. This is a sign
205    // that the snapshot should now "own" the size of this file
206    final long fakeFileSize = 2048L;
207    notifier.addArchivedFiles(ImmutableSet.of(entry(referencedFile, fakeFileSize)));
208
209    // Verify that the snapshot owns this file.
210    assertEquals(fakeFileSize, extractSnapshotSize(quotaTable, tn, snapshotName1));
211
212    // In reality, we did not actually move the file, so a "full" computation should re-set the
213    // size of the snapshot back to 0.
214    long t2 = notifier.getLastFullCompute();
215    snapshotSize = notifier.computeAndStoreSnapshotSizes(Arrays.asList(snapshotName1));
216    assertEquals(0, snapshotSize);
217    assertEquals(0, extractSnapshotSize(quotaTable, tn, snapshotName1));
218    // We should also have no recently archived files after a re-computation
219    assertTrue("Last compute time was not less than current compute time",
220        t2 < notifier.getLastFullCompute());
221  }
222
223  @Test
224  public void testParseOldNamespaceSnapshotSize() throws Exception {
225    final Admin admin = TEST_UTIL.getAdmin();
226    final TableName fakeQuotaTableName = TableName.valueOf(testName.getMethodName());
227    final TableName tn = TableName.valueOf(testName.getMethodName() + "1");
228    if (admin.tableExists(fakeQuotaTableName)) {
229      admin.disableTable(fakeQuotaTableName);
230      admin.deleteTable(fakeQuotaTableName);
231    }
232    TableDescriptor desc = TableDescriptorBuilder.newBuilder(fakeQuotaTableName).addColumnFamily(
233        ColumnFamilyDescriptorBuilder.of(QuotaTableUtil.QUOTA_FAMILY_USAGE))
234        .addColumnFamily(ColumnFamilyDescriptorBuilder.of(QuotaUtil.QUOTA_FAMILY_INFO)).build();
235    admin.createTable(desc);
236
237    final String ns = "";
238    try (Table fakeQuotaTable = conn.getTable(fakeQuotaTableName)) {
239      FileArchiverNotifierImpl notifier = new FileArchiverNotifierImpl(conn, conf, fs, tn);
240      // Verify no record is treated as zero
241      assertEquals(0, notifier.getPreviousNamespaceSnapshotSize(fakeQuotaTable, ns));
242
243      // Set an explicit value of zero
244      fakeQuotaTable.put(QuotaTableUtil.createPutForNamespaceSnapshotSize(ns, 0L));
245      assertEquals(0, notifier.getPreviousNamespaceSnapshotSize(fakeQuotaTable, ns));
246
247      // Set a non-zero value
248      fakeQuotaTable.put(QuotaTableUtil.createPutForNamespaceSnapshotSize(ns, 1024L));
249      assertEquals(1024L, notifier.getPreviousNamespaceSnapshotSize(fakeQuotaTable, ns));
250    }
251  }
252
253  private long count(Table t) throws IOException {
254    try (ResultScanner rs = t.getScanner(new Scan())) {
255      long sum = 0;
256      for (Result r : rs) {
257        while (r.advance()) {
258          sum++;
259        }
260      }
261      return sum;
262    }
263  }
264
265  private long extractSnapshotSize(
266      Table quotaTable, TableName tn, String snapshot) throws IOException {
267    Get g = QuotaTableUtil.makeGetForSnapshotSize(tn, snapshot);
268    Result r = quotaTable.get(g);
269    assertNotNull(r);
270    CellScanner cs = r.cellScanner();
271    assertTrue(cs.advance());
272    Cell c = cs.current();
273    assertNotNull(c);
274    return QuotaTableUtil.extractSnapshotSize(
275        c.getValueArray(), c.getValueOffset(), c.getValueLength());
276  }
277
278  private void verify(Table t, IOThrowingRunnable test) throws IOException {
279    admin.disableTable(t.getName());
280    admin.truncateTable(t.getName(), false);
281    test.run();
282  }
283
284  @FunctionalInterface
285  private interface IOThrowingRunnable {
286    void run() throws IOException;
287  }
288
289  private Set<String> getFilesReferencedBySnapshot(String snapshotName) throws IOException {
290    HashSet<String> files = new HashSet<>();
291    Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(
292        snapshotName, FSUtils.getRootDir(conf));
293    SnapshotProtos.SnapshotDescription sd = SnapshotDescriptionUtils.readSnapshotInfo(
294        fs, snapshotDir);
295    SnapshotManifest manifest = SnapshotManifest.open(conf, fs, snapshotDir, sd);
296    // For each region referenced by the snapshot
297    for (SnapshotRegionManifest rm : manifest.getRegionManifests()) {
298      // For each column family in this region
299      for (FamilyFiles ff : rm.getFamilyFilesList()) {
300        // And each store file in that family
301        for (StoreFile sf : ff.getStoreFilesList()) {
302          files.add(sf.getName());
303        }
304      }
305    }
306    return files;
307  }
308
309  private <K,V> Entry<K,V> entry(K k, V v) {
310    return Maps.immutableEntry(k, v);
311  }
312}