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.assertFalse; 022import static org.junit.jupiter.api.Assertions.assertNotNull; 023import static org.junit.jupiter.api.Assertions.assertTrue; 024 025import java.io.IOException; 026import java.util.Arrays; 027import java.util.Collection; 028import java.util.HashSet; 029import java.util.Map; 030import java.util.Map.Entry; 031import java.util.Set; 032import java.util.concurrent.TimeUnit; 033import java.util.concurrent.atomic.AtomicLong; 034import java.util.concurrent.atomic.AtomicReference; 035import org.apache.hadoop.conf.Configuration; 036import org.apache.hadoop.fs.FileSystem; 037import org.apache.hadoop.hbase.Cell; 038import org.apache.hadoop.hbase.HBaseTestingUtil; 039import org.apache.hadoop.hbase.NamespaceDescriptor; 040import org.apache.hadoop.hbase.TableName; 041import org.apache.hadoop.hbase.Waiter.Predicate; 042import org.apache.hadoop.hbase.client.Admin; 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.SnapshotDescription; 047import org.apache.hadoop.hbase.client.SnapshotType; 048import org.apache.hadoop.hbase.client.Table; 049import org.apache.hadoop.hbase.master.HMaster; 050import org.apache.hadoop.hbase.quotas.SpaceQuotaHelperForTests.NoFilesToDischarge; 051import org.apache.hadoop.hbase.quotas.SpaceQuotaHelperForTests.SpaceQuotaSnapshotPredicate; 052import org.apache.hadoop.hbase.regionserver.HStore; 053import org.apache.hadoop.hbase.testclassification.LargeTests; 054import org.junit.jupiter.api.AfterAll; 055import org.junit.jupiter.api.BeforeAll; 056import org.junit.jupiter.api.BeforeEach; 057import org.junit.jupiter.api.Tag; 058import org.junit.jupiter.api.Test; 059import org.junit.jupiter.api.TestInfo; 060import org.slf4j.Logger; 061import org.slf4j.LoggerFactory; 062 063import org.apache.hbase.thirdparty.com.google.common.collect.HashMultimap; 064import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap; 065import org.apache.hbase.thirdparty.com.google.common.collect.Multimap; 066 067/** 068 * Test class for the {@link SnapshotQuotaObserverChore}. 069 */ 070@Tag(LargeTests.TAG) 071public class TestSnapshotQuotaObserverChore { 072 073 private static final Logger LOG = LoggerFactory.getLogger(TestSnapshotQuotaObserverChore.class); 074 private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 075 private static final AtomicLong COUNTER = new AtomicLong(); 076 077 private Connection conn; 078 private Admin admin; 079 private SpaceQuotaHelperForTests helper; 080 private HMaster master; 081 private SnapshotQuotaObserverChore testChore; 082 private String testName; 083 084 @BeforeAll 085 public static void setUp() throws Exception { 086 Configuration conf = TEST_UTIL.getConfiguration(); 087 SpaceQuotaHelperForTests.updateConfigForQuotas(conf); 088 // Clean up the compacted files faster than normal (15s instead of 2mins) 089 conf.setInt("hbase.hfile.compaction.discharger.interval", 15 * 1000); 090 TEST_UTIL.startMiniCluster(1); 091 } 092 093 @AfterAll 094 public static void tearDown() throws Exception { 095 TEST_UTIL.shutdownMiniCluster(); 096 } 097 098 @BeforeEach 099 public void setup(TestInfo testInfo) throws Exception { 100 testName = testInfo.getTestMethod().get().getName(); 101 conn = TEST_UTIL.getConnection(); 102 admin = TEST_UTIL.getAdmin(); 103 helper = new SpaceQuotaHelperForTests(TEST_UTIL, () -> testName, COUNTER); 104 master = TEST_UTIL.getHBaseCluster().getMaster(); 105 helper.removeAllQuotas(conn); 106 testChore = new SnapshotQuotaObserverChore(TEST_UTIL.getConnection(), 107 TEST_UTIL.getConfiguration(), master.getFileSystem(), master, null); 108 } 109 110 @Test 111 public void testSnapshotsFromTables() throws Exception { 112 TableName tn1 = helper.createTableWithRegions(1); 113 TableName tn2 = helper.createTableWithRegions(1); 114 TableName tn3 = helper.createTableWithRegions(1); 115 116 // Set a space quota on table 1 and 2 (but not 3) 117 admin.setQuota(QuotaSettingsFactory.limitTableSpace(tn1, SpaceQuotaHelperForTests.ONE_GIGABYTE, 118 SpaceViolationPolicy.NO_INSERTS)); 119 admin.setQuota(QuotaSettingsFactory.limitTableSpace(tn2, SpaceQuotaHelperForTests.ONE_GIGABYTE, 120 SpaceViolationPolicy.NO_INSERTS)); 121 122 // Create snapshots on each table (we didn't write any data, so just skipflush) 123 admin.snapshot(new SnapshotDescription(tn1 + "snapshot", tn1, SnapshotType.SKIPFLUSH)); 124 admin.snapshot(new SnapshotDescription(tn2 + "snapshot", tn2, SnapshotType.SKIPFLUSH)); 125 admin.snapshot(new SnapshotDescription(tn3 + "snapshot", tn3, SnapshotType.SKIPFLUSH)); 126 127 Multimap<TableName, String> mapping = testChore.getSnapshotsToComputeSize(); 128 assertEquals(2, mapping.size()); 129 assertEquals(1, mapping.get(tn1).size()); 130 assertEquals(tn1 + "snapshot", mapping.get(tn1).iterator().next()); 131 assertEquals(1, mapping.get(tn2).size()); 132 assertEquals(tn2 + "snapshot", mapping.get(tn2).iterator().next()); 133 134 admin.snapshot(new SnapshotDescription(tn2 + "snapshot1", tn2, SnapshotType.SKIPFLUSH)); 135 admin.snapshot(new SnapshotDescription(tn3 + "snapshot1", tn3, SnapshotType.SKIPFLUSH)); 136 137 mapping = testChore.getSnapshotsToComputeSize(); 138 assertEquals(3, mapping.size()); 139 assertEquals(1, mapping.get(tn1).size()); 140 assertEquals(tn1 + "snapshot", mapping.get(tn1).iterator().next()); 141 assertEquals(2, mapping.get(tn2).size()); 142 assertEquals(new HashSet<String>(Arrays.asList(tn2 + "snapshot", tn2 + "snapshot1")), 143 mapping.get(tn2)); 144 } 145 146 @Test 147 public void testSnapshotsFromNamespaces() throws Exception { 148 NamespaceDescriptor ns = NamespaceDescriptor.create("snapshots_from_namespaces").build(); 149 admin.createNamespace(ns); 150 151 TableName tn1 = helper.createTableWithRegions(ns.getName(), 1); 152 TableName tn2 = helper.createTableWithRegions(ns.getName(), 1); 153 TableName tn3 = helper.createTableWithRegions(1); 154 155 // Set a throttle quota on 'default' namespace 156 admin.setQuota(QuotaSettingsFactory.throttleNamespace(tn3.getNamespaceAsString(), 157 ThrottleType.WRITE_NUMBER, 100, TimeUnit.SECONDS)); 158 // Set a user throttle quota 159 admin.setQuota( 160 QuotaSettingsFactory.throttleUser("user", ThrottleType.WRITE_NUMBER, 100, TimeUnit.MINUTES)); 161 162 // Set a space quota on the namespace 163 admin.setQuota(QuotaSettingsFactory.limitNamespaceSpace(ns.getName(), 164 SpaceQuotaHelperForTests.ONE_GIGABYTE, SpaceViolationPolicy.NO_INSERTS)); 165 166 // Create snapshots on each table (we didn't write any data, so just skipflush) 167 admin.snapshot(new SnapshotDescription(tn1.getQualifierAsString() + "snapshot", tn1, 168 SnapshotType.SKIPFLUSH)); 169 admin.snapshot(new SnapshotDescription(tn2.getQualifierAsString() + "snapshot", tn2, 170 SnapshotType.SKIPFLUSH)); 171 admin.snapshot(new SnapshotDescription(tn3.getQualifierAsString() + "snapshot", tn3, 172 SnapshotType.SKIPFLUSH)); 173 174 Multimap<TableName, String> mapping = testChore.getSnapshotsToComputeSize(); 175 assertEquals(2, mapping.size()); 176 assertEquals(1, mapping.get(tn1).size()); 177 assertEquals(tn1.getQualifierAsString() + "snapshot", mapping.get(tn1).iterator().next()); 178 assertEquals(1, mapping.get(tn2).size()); 179 assertEquals(tn2.getQualifierAsString() + "snapshot", mapping.get(tn2).iterator().next()); 180 181 admin.snapshot(new SnapshotDescription(tn2.getQualifierAsString() + "snapshot1", tn2, 182 SnapshotType.SKIPFLUSH)); 183 admin.snapshot(new SnapshotDescription(tn3.getQualifierAsString() + "snapshot2", tn3, 184 SnapshotType.SKIPFLUSH)); 185 186 mapping = testChore.getSnapshotsToComputeSize(); 187 assertEquals(3, mapping.size()); 188 assertEquals(1, mapping.get(tn1).size()); 189 assertEquals(tn1.getQualifierAsString() + "snapshot", mapping.get(tn1).iterator().next()); 190 assertEquals(2, mapping.get(tn2).size()); 191 assertEquals(new HashSet<String>(Arrays.asList(tn2.getQualifierAsString() + "snapshot", 192 tn2.getQualifierAsString() + "snapshot1")), mapping.get(tn2)); 193 } 194 195 @Test 196 public void testSnapshotSize() throws Exception { 197 // Create a table and set a quota 198 TableName tn1 = helper.createTableWithRegions(5); 199 admin.setQuota(QuotaSettingsFactory.limitTableSpace(tn1, SpaceQuotaHelperForTests.ONE_GIGABYTE, 200 SpaceViolationPolicy.NO_INSERTS)); 201 202 // Write some data and flush it 203 helper.writeData(tn1, 256L * SpaceQuotaHelperForTests.ONE_KILOBYTE); 204 admin.flush(tn1); 205 206 final long snapshotSize = TEST_UTIL.getMiniHBaseCluster().getRegions(tn1).stream() 207 .flatMap(r -> r.getStores().stream()).mapToLong(HStore::getHFilesSize).sum(); 208 209 // Wait for the Master chore to run to see the usage (with a fudge factor) 210 TEST_UTIL.waitFor(30_000, new SpaceQuotaSnapshotPredicate(conn, tn1) { 211 @Override 212 boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception { 213 return snapshot.getUsage() == snapshotSize; 214 } 215 }); 216 217 // Create a snapshot on the table 218 final String snapshotName = tn1 + "snapshot"; 219 admin.snapshot(new SnapshotDescription(snapshotName, tn1, SnapshotType.SKIPFLUSH)); 220 221 // Get the snapshots 222 Multimap<TableName, String> snapshotsToCompute = testChore.getSnapshotsToComputeSize(); 223 assertEquals(1, snapshotsToCompute.size(), 224 "Expected to see the single snapshot: " + snapshotsToCompute); 225 226 // Get the size of our snapshot 227 Map<String, Long> namespaceSnapshotSizes = testChore.computeSnapshotSizes(snapshotsToCompute); 228 assertEquals(1, namespaceSnapshotSizes.size()); 229 Long size = namespaceSnapshotSizes.get(tn1.getNamespaceAsString()); 230 assertNotNull(size); 231 // The snapshot should take up no space since the table refers to it completely 232 assertEquals(0, size.longValue()); 233 234 // Write some more data, flush it, and then major_compact the table 235 helper.writeData(tn1, 256L * SpaceQuotaHelperForTests.ONE_KILOBYTE); 236 admin.flush(tn1); 237 TEST_UTIL.compact(tn1, true); 238 239 // Test table should reflect it's original size since ingest was deterministic 240 TEST_UTIL.waitFor(30_000, new SpaceQuotaSnapshotPredicate(conn, tn1) { 241 private final long regionSize = TEST_UTIL.getMiniHBaseCluster().getRegions(tn1).stream() 242 .flatMap(r -> r.getStores().stream()).mapToLong(HStore::getHFilesSize).sum(); 243 244 @Override 245 boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception { 246 LOG.debug("Current usage=" + snapshot.getUsage() + " snapshotSize=" + snapshotSize); 247 // The usage of table space consists of region size and snapshot size 248 return closeInSize(snapshot.getUsage(), snapshotSize + regionSize, 249 SpaceQuotaHelperForTests.ONE_KILOBYTE); 250 } 251 }); 252 253 // Wait for no compacted files on the regions of our table 254 TEST_UTIL.waitFor(30_000, new NoFilesToDischarge(TEST_UTIL.getMiniHBaseCluster(), tn1)); 255 256 // Still should see only one snapshot 257 snapshotsToCompute = testChore.getSnapshotsToComputeSize(); 258 assertEquals(1, snapshotsToCompute.size(), 259 "Expected to see the single snapshot: " + snapshotsToCompute); 260 namespaceSnapshotSizes = testChore.computeSnapshotSizes(snapshotsToCompute); 261 assertEquals(1, namespaceSnapshotSizes.size()); 262 size = namespaceSnapshotSizes.get(tn1.getNamespaceAsString()); 263 assertNotNull(size); 264 // The snapshot should take up the size the table originally took up 265 assertEquals(snapshotSize, size.longValue()); 266 } 267 268 @Test 269 public void testPersistingSnapshotsForNamespaces() throws Exception { 270 TableName tn1 = TableName.valueOf("ns1:tn1"); 271 TableName tn2 = TableName.valueOf("ns1:tn2"); 272 TableName tn3 = TableName.valueOf("ns2:tn1"); 273 TableName tn4 = TableName.valueOf("ns2:tn2"); 274 TableName tn5 = TableName.valueOf("tn1"); 275 // Shim in a custom factory to avoid computing snapshot sizes. 276 FileArchiverNotifierFactory test = new FileArchiverNotifierFactory() { 277 Map<TableName, Long> tableToSize = 278 ImmutableMap.of(tn1, 1024L, tn2, 1024L, tn3, 512L, tn4, 1024L, tn5, 3072L); 279 280 @Override 281 public FileArchiverNotifier get(Connection conn, Configuration conf, FileSystem fs, 282 TableName tn) { 283 return new FileArchiverNotifier() { 284 @Override 285 public void addArchivedFiles(Set<Entry<String, Long>> fileSizes) throws IOException { 286 } 287 288 @Override 289 public long computeAndStoreSnapshotSizes(Collection<String> currentSnapshots) 290 throws IOException { 291 return tableToSize.get(tn); 292 } 293 }; 294 } 295 }; 296 try { 297 FileArchiverNotifierFactoryImpl.setInstance(test); 298 299 Multimap<TableName, String> snapshotsToCompute = HashMultimap.create(); 300 snapshotsToCompute.put(tn1, ""); 301 snapshotsToCompute.put(tn2, ""); 302 snapshotsToCompute.put(tn3, ""); 303 snapshotsToCompute.put(tn4, ""); 304 snapshotsToCompute.put(tn5, ""); 305 Map<String, Long> nsSizes = testChore.computeSnapshotSizes(snapshotsToCompute); 306 assertEquals(3, nsSizes.size()); 307 assertEquals(2048L, (long) nsSizes.get("ns1")); 308 assertEquals(1536L, (long) nsSizes.get("ns2")); 309 assertEquals(3072L, (long) nsSizes.get(NamespaceDescriptor.DEFAULT_NAMESPACE_NAME_STR)); 310 } finally { 311 FileArchiverNotifierFactoryImpl.reset(); 312 } 313 } 314 315 @Test 316 public void testRemovedSnapshots() throws Exception { 317 // Create a table and set a quota 318 TableName tn1 = helper.createTableWithRegions(1); 319 admin.setQuota(QuotaSettingsFactory.limitTableSpace(tn1, SpaceQuotaHelperForTests.ONE_GIGABYTE, 320 SpaceViolationPolicy.NO_INSERTS)); 321 322 // Write some data and flush it 323 helper.writeData(tn1, 256L * SpaceQuotaHelperForTests.ONE_KILOBYTE); // 256 KB 324 325 final AtomicReference<Long> lastSeenSize = new AtomicReference<>(); 326 // Wait for the Master chore to run to see the usage (with a fudge factor) 327 TEST_UTIL.waitFor(30_000, new SpaceQuotaSnapshotPredicate(conn, tn1) { 328 @Override 329 boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception { 330 lastSeenSize.set(snapshot.getUsage()); 331 return snapshot.getUsage() > 230L * SpaceQuotaHelperForTests.ONE_KILOBYTE; 332 } 333 }); 334 335 // Create a snapshot on the table 336 final String snapshotName1 = tn1 + "snapshot1"; 337 admin.snapshot(new SnapshotDescription(snapshotName1, tn1, SnapshotType.SKIPFLUSH)); 338 339 // Snapshot size has to be 0 as the snapshot shares the data with the table 340 final Table quotaTable = conn.getTable(QuotaUtil.QUOTA_TABLE_NAME); 341 TEST_UTIL.waitFor(30_000, new Predicate<Exception>() { 342 @Override 343 public boolean evaluate() throws Exception { 344 Get g = QuotaTableUtil.makeGetForSnapshotSize(tn1, snapshotName1); 345 Result r = quotaTable.get(g); 346 if (r == null || r.isEmpty()) { 347 return false; 348 } 349 r.advance(); 350 Cell c = r.current(); 351 return QuotaTableUtil.parseSnapshotSize(c) == 0; 352 } 353 }); 354 // Total usage has to remain same as what we saw before taking a snapshot 355 TEST_UTIL.waitFor(30_000, new SpaceQuotaSnapshotPredicate(conn, tn1) { 356 @Override 357 boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception { 358 return snapshot.getUsage() == lastSeenSize.get(); 359 } 360 }); 361 362 // Major compact the table to force a rewrite 363 TEST_UTIL.compact(tn1, true); 364 // Now the snapshot size has to prev total size 365 TEST_UTIL.waitFor(30_000, new Predicate<Exception>() { 366 @Override 367 public boolean evaluate() throws Exception { 368 Get g = QuotaTableUtil.makeGetForSnapshotSize(tn1, snapshotName1); 369 Result r = quotaTable.get(g); 370 if (r == null || r.isEmpty()) { 371 return false; 372 } 373 r.advance(); 374 Cell c = r.current(); 375 // The compaction result file has an additional compaction event tracker 376 return lastSeenSize.get() == QuotaTableUtil.parseSnapshotSize(c); 377 } 378 }); 379 // The total size now has to be equal/more than double of prev total size 380 // as double the number of store files exist now. 381 final AtomicReference<Long> sizeAfterCompaction = new AtomicReference<>(); 382 TEST_UTIL.waitFor(30_000, new SpaceQuotaSnapshotPredicate(conn, tn1) { 383 @Override 384 boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception { 385 sizeAfterCompaction.set(snapshot.getUsage()); 386 return snapshot.getUsage() >= 2 * lastSeenSize.get(); 387 } 388 }); 389 390 // Delete the snapshot 391 admin.deleteSnapshot(snapshotName1); 392 // Total size has to come down to prev totalsize - snapshot size(which was removed) 393 TEST_UTIL.waitFor(30_000, new SpaceQuotaSnapshotPredicate(conn, tn1) { 394 @Override 395 boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception { 396 return snapshot.getUsage() == (sizeAfterCompaction.get() - lastSeenSize.get()); 397 } 398 }); 399 } 400 401 @Test 402 public void testBucketingFilesToSnapshots() throws Exception { 403 // Create a table and set a quota 404 TableName tn1 = helper.createTableWithRegions(1); 405 admin.setQuota(QuotaSettingsFactory.limitTableSpace(tn1, SpaceQuotaHelperForTests.ONE_GIGABYTE, 406 SpaceViolationPolicy.NO_INSERTS)); 407 408 // Write some data and flush it 409 helper.writeData(tn1, 256L * SpaceQuotaHelperForTests.ONE_KILOBYTE); 410 admin.flush(tn1); 411 412 final AtomicReference<Long> lastSeenSize = new AtomicReference<>(); 413 // Wait for the Master chore to run to see the usage (with a fudge factor) 414 TEST_UTIL.waitFor(30_000, new SpaceQuotaSnapshotPredicate(conn, tn1) { 415 @Override 416 boolean evaluate(SpaceQuotaSnapshot snapshot) throws Exception { 417 lastSeenSize.set(snapshot.getUsage()); 418 return snapshot.getUsage() > 230L * SpaceQuotaHelperForTests.ONE_KILOBYTE; 419 } 420 }); 421 422 // Create a snapshot on the table 423 final String snapshotName1 = tn1 + "snapshot1"; 424 admin.snapshot(new SnapshotDescription(snapshotName1, tn1, SnapshotType.SKIPFLUSH)); 425 // Major compact the table to force a rewrite 426 TEST_UTIL.compact(tn1, true); 427 428 // Make sure that the snapshot owns the size 429 final Table quotaTable = conn.getTable(QuotaUtil.QUOTA_TABLE_NAME); 430 TEST_UTIL.waitFor(30_000, new Predicate<Exception>() { 431 @Override 432 public boolean evaluate() throws Exception { 433 Get g = QuotaTableUtil.makeGetForSnapshotSize(tn1, snapshotName1); 434 Result r = quotaTable.get(g); 435 if (r == null || r.isEmpty()) { 436 return false; 437 } 438 r.advance(); 439 Cell c = r.current(); 440 // The compaction result file has an additional compaction event tracker 441 return lastSeenSize.get() <= QuotaTableUtil.parseSnapshotSize(c); 442 } 443 }); 444 445 // Create another snapshot on the table 446 final String snapshotName2 = tn1 + "snapshot2"; 447 admin.snapshot(new SnapshotDescription(snapshotName2, tn1, SnapshotType.SKIPFLUSH)); 448 // Major compact the table to force a rewrite 449 TEST_UTIL.compact(tn1, true); 450 451 // Make sure that the snapshot owns the size 452 TEST_UTIL.waitFor(30_000, new Predicate<Exception>() { 453 @Override 454 public boolean evaluate() throws Exception { 455 Get g = QuotaTableUtil.makeGetForSnapshotSize(tn1, snapshotName2); 456 Result r = quotaTable.get(g); 457 if (r == null || r.isEmpty()) { 458 return false; 459 } 460 r.advance(); 461 Cell c = r.current(); 462 // The compaction result file has an additional compaction event tracker 463 return lastSeenSize.get() <= QuotaTableUtil.parseSnapshotSize(c); 464 } 465 }); 466 467 Get g = QuotaTableUtil.createGetNamespaceSnapshotSize(tn1.getNamespaceAsString()); 468 Result r = quotaTable.get(g); 469 assertNotNull(r); 470 assertFalse(r.isEmpty()); 471 r.advance(); 472 long size = QuotaTableUtil.parseSnapshotSize(r.current()); 473 assertTrue(lastSeenSize.get() * 2 <= size); 474 } 475 476 /** 477 * Computes if {@code size2} is within {@code delta} of {@code size1}, inclusive. 478 */ 479 boolean closeInSize(long size1, long size2, long delta) { 480 long lower = size1 - delta; 481 long upper = size1 + delta; 482 return lower <= size2 && size2 <= upper; 483 } 484}