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