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.quotas;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertNotNull;
022import static org.junit.jupiter.api.Assertions.assertTrue;
023
024import java.io.IOException;
025import java.util.Map;
026import java.util.concurrent.atomic.AtomicInteger;
027import java.util.concurrent.atomic.AtomicLong;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.hbase.Cell;
030import org.apache.hadoop.hbase.CellScanner;
031import org.apache.hadoop.hbase.HBaseTestingUtil;
032import org.apache.hadoop.hbase.TableName;
033import org.apache.hadoop.hbase.Waiter;
034import org.apache.hadoop.hbase.Waiter.Predicate;
035import org.apache.hadoop.hbase.client.Admin;
036import org.apache.hadoop.hbase.client.Connection;
037import org.apache.hadoop.hbase.client.Result;
038import org.apache.hadoop.hbase.client.ResultScanner;
039import org.apache.hadoop.hbase.client.Scan;
040import org.apache.hadoop.hbase.client.SnapshotType;
041import org.apache.hadoop.hbase.client.Table;
042import org.apache.hadoop.hbase.quotas.SpaceQuotaHelperForTests.SpaceQuotaSnapshotPredicate;
043import org.apache.hadoop.hbase.testclassification.LargeTests;
044import org.junit.jupiter.api.AfterAll;
045import org.junit.jupiter.api.BeforeAll;
046import org.junit.jupiter.api.BeforeEach;
047import org.junit.jupiter.api.Tag;
048import org.junit.jupiter.api.Test;
049import org.junit.jupiter.api.TestInfo;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053import org.apache.hbase.thirdparty.com.google.common.collect.Iterables;
054import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations;
055
056import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos;
057
058/**
059 * Test class to exercise the inclusion of snapshots in space quotas
060 */
061@Tag(LargeTests.TAG)
062public class TestSpaceQuotasWithSnapshots {
063
064  private static final Logger LOG = LoggerFactory.getLogger(TestSpaceQuotasWithSnapshots.class);
065  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
066  // Global for all tests in the class
067  private static final AtomicLong COUNTER = new AtomicLong(0);
068  private static final long FUDGE_FOR_TABLE_SIZE = 500L * SpaceQuotaHelperForTests.ONE_KILOBYTE;
069
070  private SpaceQuotaHelperForTests helper;
071  private Connection conn;
072  private Admin admin;
073
074  @BeforeAll
075  public static void setUp() throws Exception {
076    Configuration conf = TEST_UTIL.getConfiguration();
077    SpaceQuotaHelperForTests.updateConfigForQuotas(conf);
078    TEST_UTIL.startMiniCluster(1);
079    // Wait till quota table onlined.
080    TEST_UTIL.waitFor(10000, new Waiter.Predicate<Exception>() {
081      @Override
082      public boolean evaluate() throws Exception {
083        return TEST_UTIL.getAdmin().tableExists(QuotaTableUtil.QUOTA_TABLE_NAME);
084      }
085    });
086  }
087
088  @AfterAll
089  public static void tearDown() throws Exception {
090    TEST_UTIL.shutdownMiniCluster();
091  }
092
093  @BeforeEach
094  public void removeAllQuotas(TestInfo testInfo) throws Exception {
095    helper = new SpaceQuotaHelperForTests(TEST_UTIL, () -> testInfo.getTestMethod().get().getName(),
096      COUNTER);
097    conn = TEST_UTIL.getConnection();
098    admin = TEST_UTIL.getAdmin();
099  }
100
101  @Test
102  public void testTablesInheritSnapshotSize() throws Exception {
103    TableName tn = helper.createTableWithRegions(1);
104    LOG.info("Writing data");
105    // Set a quota
106    QuotaSettings settings = QuotaSettingsFactory.limitTableSpace(tn,
107      SpaceQuotaHelperForTests.ONE_GIGABYTE, SpaceViolationPolicy.NO_INSERTS);
108    admin.setQuota(settings);
109    // Write some data
110    final long initialSize = 2L * SpaceQuotaHelperForTests.ONE_MEGABYTE;
111    helper.writeData(tn, initialSize);
112
113    LOG.info("Waiting until table size reflects written data");
114    // Wait until that data is seen by the master
115    TEST_UTIL.waitFor(30 * 1000, 500, new SpaceQuotaSnapshotPredicate(conn, tn) {
116      @Override
117      boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
118        return snapshot.getUsage() >= initialSize;
119      }
120    });
121
122    // Make sure we see the final quota usage size
123    waitForStableQuotaSize(conn, tn, null);
124
125    // The actual size on disk after we wrote our data the first time
126    final long actualInitialSize = conn.getAdmin().getCurrentSpaceQuotaSnapshot(tn).getUsage();
127    LOG.info("Initial table size was " + actualInitialSize);
128
129    LOG.info("Snapshot the table");
130    final String snapshot1 = tn.toString() + "_snapshot1";
131    admin.snapshot(snapshot1, tn);
132
133    // Write the same data again, then flush+compact. This should make sure that
134    // the snapshot is referencing files that the table no longer references.
135    LOG.info("Write more data");
136    helper.writeData(tn, initialSize);
137    LOG.info("Flush the table");
138    admin.flush(tn);
139    LOG.info("Synchronously compacting the table");
140    TEST_UTIL.compact(tn, true);
141
142    final long upperBound = initialSize + FUDGE_FOR_TABLE_SIZE;
143    final long lowerBound = initialSize - FUDGE_FOR_TABLE_SIZE;
144
145    // Store the actual size after writing more data and then compacting it down to one file
146    LOG.info("Waiting for the region reports to reflect the correct size, between (" + lowerBound
147      + ", " + upperBound + ")");
148    TEST_UTIL.waitFor(30 * 1000, 500, new Predicate<Exception>() {
149      @Override
150      public boolean evaluate() throws Exception {
151        long size = getRegionSizeReportForTable(conn, tn);
152        return size < upperBound && size > lowerBound;
153      }
154    });
155
156    // Make sure we see the "final" new size for the table, not some intermediate
157    waitForStableRegionSizeReport(conn, tn);
158    final long finalSize = getRegionSizeReportForTable(conn, tn);
159    assertTrue(finalSize > 0, "Table data size must be greater than zero");
160    LOG.info("Last seen size: " + finalSize);
161
162    // Make sure the QuotaObserverChore has time to reflect the new region size reports
163    // (we saw above). The usage of the table should *not* decrease when we check it below,
164    // though, because the snapshot on our table will cause the table to "retain" the size.
165    TEST_UTIL.waitFor(20 * 1000, 500, new SpaceQuotaSnapshotPredicate(conn, tn) {
166      @Override
167      public boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
168        return snapshot.getUsage() >= finalSize;
169      }
170    });
171
172    // The final usage should be the sum of the initial size (referenced by the snapshot) and the
173    // new size we just wrote above.
174    long expectedFinalSize = actualInitialSize + finalSize;
175    LOG.info("Expecting table usage to be " + actualInitialSize + " + " + finalSize + " = "
176      + expectedFinalSize);
177    // The size of the table (WRT quotas) should now be approximately double what it was previously
178    TEST_UTIL.waitFor(30 * 1000, 1000, new SpaceQuotaSnapshotPredicate(conn, tn) {
179      @Override
180      boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
181        LOG.debug("Checking for " + expectedFinalSize + " == " + snapshot.getUsage());
182        return expectedFinalSize == snapshot.getUsage();
183      }
184    });
185
186    Map<String, Long> snapshotSizes = QuotaTableUtil.getObservedSnapshotSizes(conn);
187    Long size = snapshotSizes.get(snapshot1);
188    assertNotNull(size, "Did not observe the size of the snapshot");
189    assertEquals(actualInitialSize, size.longValue(),
190      "The recorded size of the HBase snapshot was not the size we expected");
191  }
192
193  @Test
194  public void testNamespacesInheritSnapshotSize() throws Exception {
195    String ns = helper.createNamespace().getName();
196    TableName tn = helper.createTableWithRegions(ns, 1);
197    LOG.info("Writing data");
198    // Set a quota
199    QuotaSettings settings = QuotaSettingsFactory.limitNamespaceSpace(ns,
200      SpaceQuotaHelperForTests.ONE_GIGABYTE, SpaceViolationPolicy.NO_INSERTS);
201    admin.setQuota(settings);
202
203    // Write some data and flush it to disk
204    final long initialSize = 2L * SpaceQuotaHelperForTests.ONE_MEGABYTE;
205    helper.writeData(tn, initialSize);
206    admin.flush(tn);
207
208    LOG.info("Waiting until namespace size reflects written data");
209    // Wait until that data is seen by the master
210    TEST_UTIL.waitFor(30 * 1000, 500, new SpaceQuotaSnapshotPredicate(conn, ns) {
211      @Override
212      boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
213        return snapshot.getUsage() >= initialSize;
214      }
215    });
216
217    // Make sure we see the "final" new size for the table, not some intermediate
218    waitForStableQuotaSize(conn, null, ns);
219
220    // The actual size on disk after we wrote our data the first time
221    final long actualInitialSize = conn.getAdmin().getCurrentSpaceQuotaSnapshot(ns).getUsage();
222    LOG.info("Initial table size was " + actualInitialSize);
223
224    LOG.info("Snapshot the table");
225    final String snapshot1 = tn.getQualifierAsString() + "_snapshot1";
226    admin.snapshot(snapshot1, tn);
227
228    // Write the same data again, then flush+compact. This should make sure that
229    // the snapshot is referencing files that the table no longer references.
230    LOG.info("Write more data");
231    helper.writeData(tn, initialSize);
232    LOG.info("Flush the table");
233    admin.flush(tn);
234    LOG.info("Synchronously compacting the table");
235    TEST_UTIL.compact(tn, true);
236
237    final long upperBound = initialSize + FUDGE_FOR_TABLE_SIZE;
238    final long lowerBound = initialSize - FUDGE_FOR_TABLE_SIZE;
239
240    LOG.info("Waiting for the region reports to reflect the correct size, between (" + lowerBound
241      + ", " + upperBound + ")");
242    TEST_UTIL.waitFor(30 * 1000, 500, new Predicate<Exception>() {
243      @Override
244      public boolean evaluate() throws Exception {
245        Map<TableName, Long> sizes = conn.getAdmin().getSpaceQuotaTableSizes();
246        LOG.debug("Master observed table sizes from region size reports: " + sizes);
247        Long size = sizes.get(tn);
248        if (null == size) {
249          return false;
250        }
251        return size < upperBound && size > lowerBound;
252      }
253    });
254
255    // Make sure we see the "final" new size for the table, not some intermediate
256    waitForStableRegionSizeReport(conn, tn);
257    final long finalSize = getRegionSizeReportForTable(conn, tn);
258    assertTrue(finalSize > 0, "Table data size must be greater than zero");
259    LOG.info("Final observed size of table: " + finalSize);
260
261    // Make sure the QuotaObserverChore has time to reflect the new region size reports
262    // (we saw above). The usage of the table should *not* decrease when we check it below,
263    // though, because the snapshot on our table will cause the table to "retain" the size.
264    TEST_UTIL.waitFor(20 * 1000, 500, new SpaceQuotaSnapshotPredicate(conn, ns) {
265      @Override
266      public boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
267        return snapshot.getUsage() >= finalSize;
268      }
269    });
270
271    // The final usage should be the sum of the initial size (referenced by the snapshot) and the
272    // new size we just wrote above.
273    long expectedFinalSize = actualInitialSize + finalSize;
274    LOG.info("Expecting namespace usage to be " + actualInitialSize + " + " + finalSize + " = "
275      + expectedFinalSize);
276    // The size of the table (WRT quotas) should now be approximately double what it was previously
277    TEST_UTIL.waitFor(30 * 1000, 1000, new SpaceQuotaSnapshotPredicate(conn, ns) {
278      @Override
279      boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
280        LOG.debug("Checking for " + expectedFinalSize + " == " + snapshot.getUsage());
281        return expectedFinalSize == snapshot.getUsage();
282      }
283    });
284
285    Map<String, Long> snapshotSizes = QuotaTableUtil.getObservedSnapshotSizes(conn);
286    Long size = snapshotSizes.get(snapshot1);
287    assertNotNull(size, "Did not observe the size of the snapshot");
288    assertEquals(actualInitialSize, size.longValue(),
289      "The recorded size of the HBase snapshot was not the size we expected");
290  }
291
292  @Test
293  public void testTablesWithSnapshots() throws Exception {
294    final Connection conn = TEST_UTIL.getConnection();
295    final SpaceViolationPolicy policy = SpaceViolationPolicy.NO_INSERTS;
296    final TableName tn = helper.createTableWithRegions(10);
297
298    // 3MB limit on the table
299    final long tableLimit = 3L * SpaceQuotaHelperForTests.ONE_MEGABYTE;
300    TEST_UTIL.getAdmin().setQuota(QuotaSettingsFactory.limitTableSpace(tn, tableLimit, policy));
301
302    LOG.info("Writing first data set");
303    // Write more data than should be allowed and flush it to disk
304    helper.writeData(tn, 1L * SpaceQuotaHelperForTests.ONE_MEGABYTE, "q1");
305
306    LOG.info("Creating snapshot");
307    TEST_UTIL.getAdmin().snapshot(tn.toString() + "snap1", tn, SnapshotType.FLUSH);
308
309    LOG.info("Writing second data set");
310    // Write some more data
311    helper.writeData(tn, 1L * SpaceQuotaHelperForTests.ONE_MEGABYTE, "q2");
312
313    LOG.info("Flushing and major compacting table");
314    // Compact the table to force the snapshot to own all of its files
315    TEST_UTIL.getAdmin().flush(tn);
316    TEST_UTIL.compact(tn, true);
317
318    LOG.info("Checking for quota violation");
319    // Wait to observe the quota moving into violation
320    TEST_UTIL.waitFor(60_000, 1_000, new Predicate<Exception>() {
321      @Override
322      public boolean evaluate() throws Exception {
323        Scan s = QuotaTableUtil.makeQuotaSnapshotScanForTable(tn);
324        try (Table t = conn.getTable(QuotaTableUtil.QUOTA_TABLE_NAME)) {
325          ResultScanner rs = t.getScanner(s);
326          try {
327            Result r = Iterables.getOnlyElement(rs);
328            CellScanner cs = r.cellScanner();
329            assertTrue(cs.advance());
330            Cell c = cs.current();
331            SpaceQuotaSnapshot snapshot = SpaceQuotaSnapshot
332              .toSpaceQuotaSnapshot(QuotaProtos.SpaceQuotaSnapshot.parseFrom(UnsafeByteOperations
333                .unsafeWrap(c.getValueArray(), c.getValueOffset(), c.getValueLength())));
334            LOG.info(
335              snapshot.getUsage() + "/" + snapshot.getLimit() + " " + snapshot.getQuotaStatus());
336            // We expect to see the table move to violation
337            return snapshot.getQuotaStatus().isInViolation();
338          } finally {
339            if (null != rs) {
340              rs.close();
341            }
342          }
343        }
344      }
345    });
346  }
347
348  @Test
349  public void testRematerializedTablesDoNoInheritSpace() throws Exception {
350    TableName tn = helper.createTableWithRegions(1);
351    TableName tn2 = helper.getNextTableName();
352    LOG.info("Writing data");
353    // Set a quota on both tables
354    QuotaSettings settings = QuotaSettingsFactory.limitTableSpace(tn,
355      SpaceQuotaHelperForTests.ONE_GIGABYTE, SpaceViolationPolicy.NO_INSERTS);
356    admin.setQuota(settings);
357    QuotaSettings settings2 = QuotaSettingsFactory.limitTableSpace(tn2,
358      SpaceQuotaHelperForTests.ONE_GIGABYTE, SpaceViolationPolicy.NO_INSERTS);
359    admin.setQuota(settings2);
360    // Write some data
361    final long initialSize = 2L * SpaceQuotaHelperForTests.ONE_MEGABYTE;
362    helper.writeData(tn, initialSize);
363
364    LOG.info("Waiting until table size reflects written data");
365    // Wait until that data is seen by the master
366    TEST_UTIL.waitFor(30 * 1000, 500, new SpaceQuotaSnapshotPredicate(conn, tn) {
367      @Override
368      boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
369        return snapshot.getUsage() >= initialSize;
370      }
371    });
372
373    // Make sure we see the final quota usage size
374    waitForStableQuotaSize(conn, tn, null);
375
376    // The actual size on disk after we wrote our data the first time
377    final long actualInitialSize = conn.getAdmin().getCurrentSpaceQuotaSnapshot(tn).getUsage();
378    LOG.info("Initial table size was " + actualInitialSize);
379
380    LOG.info("Snapshot the table");
381    final String snapshot1 = tn.toString() + "_snapshot1";
382    admin.snapshot(snapshot1, tn);
383
384    admin.cloneSnapshot(snapshot1, tn2);
385
386    // Write some more data to the first table
387    helper.writeData(tn, initialSize, "q2");
388    admin.flush(tn);
389
390    // Watch the usage of the first table with some more data to know when the new
391    // region size reports were sent to the master
392    TEST_UTIL.waitFor(30_000, 1_000, new SpaceQuotaSnapshotPredicate(conn, tn) {
393      @Override
394      boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
395        return snapshot.getUsage() >= actualInitialSize * 2;
396      }
397    });
398
399    // We know that reports were sent by our RS, verify that they take up zero size.
400    SpaceQuotaSnapshot snapshot =
401      (SpaceQuotaSnapshot) conn.getAdmin().getCurrentSpaceQuotaSnapshot(tn2);
402    assertNotNull(snapshot);
403    assertEquals(0, snapshot.getUsage());
404
405    // Compact the cloned table to force it to own its own files.
406    TEST_UTIL.compact(tn2, true);
407    // After the table is compacted, it should have its own files and be the same size as originally
408    // But The compaction result file has an additional compaction event tracker
409    TEST_UTIL.waitFor(30_000, 1_000, new SpaceQuotaSnapshotPredicate(conn, tn2) {
410      @Override
411      boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
412        return snapshot.getUsage() >= actualInitialSize;
413      }
414    });
415  }
416
417  void waitForStableQuotaSize(Connection conn, TableName tn, String ns) throws Exception {
418    // For some stability in the value before proceeding
419    // Helps make sure that we got the actual last value, not some inbetween
420    AtomicLong lastValue = new AtomicLong(-1);
421    AtomicInteger counter = new AtomicInteger(0);
422    TEST_UTIL.waitFor(15_000, 500, new SpaceQuotaSnapshotPredicate(conn, tn, ns) {
423      @Override
424      boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception {
425        LOG.debug("Last observed size=" + lastValue.get());
426        if (snapshot.getUsage() == lastValue.get()) {
427          int numMatches = counter.incrementAndGet();
428          if (numMatches >= 5) {
429            return true;
430          }
431          // Not yet..
432          return false;
433        }
434        counter.set(0);
435        lastValue.set(snapshot.getUsage());
436        return false;
437      }
438    });
439  }
440
441  long getRegionSizeReportForTable(Connection conn, TableName tn) throws IOException {
442    Map<TableName, Long> sizes = conn.getAdmin().getSpaceQuotaTableSizes();
443    Long value = sizes.get(tn);
444    if (null == value) {
445      return 0L;
446    }
447    return value.longValue();
448  }
449
450  void waitForStableRegionSizeReport(Connection conn, TableName tn) throws Exception {
451    // For some stability in the value before proceeding
452    // Helps make sure that we got the actual last value, not some inbetween
453    AtomicLong lastValue = new AtomicLong(-1);
454    AtomicInteger counter = new AtomicInteger(0);
455    TEST_UTIL.waitFor(15_000, 500, new Predicate<Exception>() {
456      @Override
457      public boolean evaluate() throws Exception {
458        LOG.debug("Last observed size=" + lastValue.get());
459        long actual = getRegionSizeReportForTable(conn, tn);
460        if (actual == lastValue.get()) {
461          int numMatches = counter.incrementAndGet();
462          if (numMatches >= 5) {
463            return true;
464          }
465          // Not yet..
466          return false;
467        }
468        counter.set(0);
469        lastValue.set(actual);
470        return false;
471      }
472    });
473  }
474}