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}