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; 022 023import java.io.IOException; 024import java.util.Arrays; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Map.Entry; 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.hbase.HBaseTestingUtil; 031import org.apache.hadoop.hbase.TableName; 032import org.apache.hadoop.hbase.Waiter; 033import org.apache.hadoop.hbase.client.Admin; 034import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 035import org.apache.hadoop.hbase.client.Connection; 036import org.apache.hadoop.hbase.client.Put; 037import org.apache.hadoop.hbase.client.RegionInfo; 038import org.apache.hadoop.hbase.client.Result; 039import org.apache.hadoop.hbase.client.ResultScanner; 040import org.apache.hadoop.hbase.client.Table; 041import org.apache.hadoop.hbase.client.TableDescriptor; 042import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 043import org.apache.hadoop.hbase.master.HMaster; 044import org.apache.hadoop.hbase.testclassification.LargeTests; 045import org.apache.hadoop.hbase.util.Bytes; 046import org.junit.jupiter.api.AfterEach; 047import org.junit.jupiter.api.BeforeEach; 048import org.junit.jupiter.api.Tag; 049import org.junit.jupiter.api.Test; 050import org.slf4j.Logger; 051import org.slf4j.LoggerFactory; 052 053/** 054 * A test case to verify that region reports are expired when they are not sent. 055 */ 056@Tag(LargeTests.TAG) 057public class TestQuotaObserverChoreRegionReports { 058 059 private static final Logger LOG = 060 LoggerFactory.getLogger(TestQuotaObserverChoreRegionReports.class); 061 private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 062 063 @BeforeEach 064 public void setUp() throws Exception { 065 Configuration conf = TEST_UTIL.getConfiguration(); 066 // Increase the frequency of some of the chores for responsiveness of the test 067 SpaceQuotaHelperForTests.updateConfigForQuotas(conf); 068 conf.setInt(QuotaObserverChore.REGION_REPORT_RETENTION_DURATION_KEY, 1000); 069 } 070 071 @AfterEach 072 public void tearDown() throws Exception { 073 TEST_UTIL.shutdownMiniCluster(); 074 } 075 076 @Test 077 public void testReportExpiration() throws Exception { 078 Configuration conf = TEST_UTIL.getConfiguration(); 079 // Send reports every 25 seconds 080 conf.setInt(RegionSizeReportingChore.REGION_SIZE_REPORTING_CHORE_PERIOD_KEY, 25000); 081 // Expire the reports after 5 seconds 082 conf.setInt(QuotaObserverChore.REGION_REPORT_RETENTION_DURATION_KEY, 5000); 083 TEST_UTIL.startMiniCluster(1); 084 // Wait till quota table onlined. 085 TEST_UTIL.waitFor(10000, new Waiter.Predicate<Exception>() { 086 @Override 087 public boolean evaluate() throws Exception { 088 return TEST_UTIL.getAdmin().tableExists(QuotaTableUtil.QUOTA_TABLE_NAME); 089 } 090 }); 091 092 final String FAM1 = "f1"; 093 final HMaster master = TEST_UTIL.getMiniHBaseCluster().getMaster(); 094 // Wait for the master to finish initialization. 095 while (master.getMasterQuotaManager() == null) { 096 LOG.debug("MasterQuotaManager is null, waiting..."); 097 Thread.sleep(500); 098 } 099 final MasterQuotaManager quotaManager = master.getMasterQuotaManager(); 100 101 // Create a table 102 final TableName tn = TableName.valueOf("reportExpiration"); 103 TableDescriptor tableDesc = TableDescriptorBuilder.newBuilder(tn) 104 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAM1)).build(); 105 TEST_UTIL.getAdmin().createTable(tableDesc); 106 107 // No reports right after we created this table. 108 assertEquals(0, getRegionReportsForTable(quotaManager.snapshotRegionSizes(), tn)); 109 110 // Set a quota 111 final long sizeLimit = 100L * SpaceQuotaHelperForTests.ONE_MEGABYTE; 112 final SpaceViolationPolicy violationPolicy = SpaceViolationPolicy.NO_INSERTS; 113 QuotaSettings settings = QuotaSettingsFactory.limitTableSpace(tn, sizeLimit, violationPolicy); 114 TEST_UTIL.getAdmin().setQuota(settings); 115 116 // We should get one report for the one region we have. 117 Waiter.waitFor(TEST_UTIL.getConfiguration(), 45000, 1000, new Waiter.Predicate<Exception>() { 118 @Override 119 public boolean evaluate() throws Exception { 120 int numReports = getRegionReportsForTable(quotaManager.snapshotRegionSizes(), tn); 121 LOG.debug("Saw " + numReports + " reports for " + tn + " while waiting for 1"); 122 return numReports == 1; 123 } 124 }); 125 126 // We should then see no reports for the single region 127 Waiter.waitFor(TEST_UTIL.getConfiguration(), 15000, 1000, new Waiter.Predicate<Exception>() { 128 @Override 129 public boolean evaluate() throws Exception { 130 int numReports = getRegionReportsForTable(quotaManager.snapshotRegionSizes(), tn); 131 LOG.debug("Saw " + numReports + " reports for " + tn + " while waiting for none"); 132 return numReports == 0; 133 } 134 }); 135 } 136 137 @Test 138 public void testMissingReportsRemovesQuota() throws Exception { 139 Configuration conf = TEST_UTIL.getConfiguration(); 140 // Expire the reports after 5 seconds 141 conf.setInt(QuotaObserverChore.REGION_REPORT_RETENTION_DURATION_KEY, 5000); 142 TEST_UTIL.startMiniCluster(1); 143 // Wait till quota table onlined. 144 TEST_UTIL.waitFor(10000, new Waiter.Predicate<Exception>() { 145 @Override 146 public boolean evaluate() throws Exception { 147 return TEST_UTIL.getAdmin().tableExists(QuotaTableUtil.QUOTA_TABLE_NAME); 148 } 149 }); 150 151 final String FAM1 = "f1"; 152 153 // Create a table 154 final TableName tn = TableName.valueOf("quotaAcceptanceWithoutReports"); 155 TableDescriptor tableDesc = TableDescriptorBuilder.newBuilder(tn) 156 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAM1)).build(); 157 TEST_UTIL.getAdmin().createTable(tableDesc); 158 159 // Set a quota 160 final long sizeLimit = 1L * SpaceQuotaHelperForTests.ONE_KILOBYTE; 161 final SpaceViolationPolicy violationPolicy = SpaceViolationPolicy.NO_INSERTS; 162 QuotaSettings settings = QuotaSettingsFactory.limitTableSpace(tn, sizeLimit, violationPolicy); 163 final Admin admin = TEST_UTIL.getAdmin(); 164 LOG.info("SET QUOTA"); 165 admin.setQuota(settings); 166 final Connection conn = TEST_UTIL.getConnection(); 167 168 // Write enough data to invalidate the quota 169 Put p = new Put(Bytes.toBytes("row1")); 170 byte[] bytes = new byte[10]; 171 Arrays.fill(bytes, (byte) 2); 172 for (int i = 0; i < 200; i++) { 173 p.addColumn(Bytes.toBytes(FAM1), Bytes.toBytes("qual" + i), bytes); 174 } 175 conn.getTable(tn).put(p); 176 admin.flush(tn); 177 178 // Wait for the table to move into violation 179 Waiter.waitFor(TEST_UTIL.getConfiguration(), 30000, 1000, new Waiter.Predicate<Exception>() { 180 @Override 181 public boolean evaluate() throws Exception { 182 SpaceQuotaSnapshot snapshot = getSnapshotForTable(conn, tn); 183 if (snapshot == null) { 184 return false; 185 } 186 return snapshot.getQuotaStatus().isInViolation(); 187 } 188 }); 189 190 // Close the region, prevent the server from sending new status reports. 191 List<RegionInfo> regions = admin.getRegions(tn); 192 assertEquals(1, regions.size()); 193 RegionInfo hri = regions.get(0); 194 admin.unassign(hri.getRegionName(), true); 195 196 // We should see this table move out of violation after the report expires. 197 Waiter.waitFor(TEST_UTIL.getConfiguration(), 30000, 1000, new Waiter.Predicate<Exception>() { 198 @Override 199 public boolean evaluate() throws Exception { 200 SpaceQuotaSnapshot snapshot = getSnapshotForTable(conn, tn); 201 if (snapshot == null) { 202 return false; 203 } 204 return !snapshot.getQuotaStatus().isInViolation(); 205 } 206 }); 207 208 // The QuotaObserverChore's memory should also show it not in violation. 209 final HMaster master = TEST_UTIL.getMiniHBaseCluster().getMaster(); 210 QuotaSnapshotStore<TableName> tableStore = 211 master.getQuotaObserverChore().getTableSnapshotStore(); 212 SpaceQuotaSnapshot snapshot = tableStore.getCurrentState(tn); 213 assertFalse(snapshot.getQuotaStatus().isInViolation(), "Quota should not be in violation"); 214 } 215 216 private SpaceQuotaSnapshot getSnapshotForTable(Connection conn, TableName tn) throws IOException { 217 try (Table quotaTable = conn.getTable(QuotaUtil.QUOTA_TABLE_NAME); 218 ResultScanner scanner = quotaTable.getScanner(QuotaTableUtil.makeQuotaSnapshotScan())) { 219 Map<TableName, SpaceQuotaSnapshot> activeViolations = new HashMap<>(); 220 for (Result result : scanner) { 221 try { 222 QuotaTableUtil.extractQuotaSnapshot(result, activeViolations); 223 } catch (IllegalArgumentException e) { 224 final String msg = "Failed to parse result for row " + Bytes.toString(result.getRow()); 225 LOG.error(msg, e); 226 throw new IOException(msg, e); 227 } 228 } 229 return activeViolations.get(tn); 230 } 231 } 232 233 private int getRegionReportsForTable(Map<RegionInfo, Long> reports, TableName tn) { 234 int numReports = 0; 235 for (Entry<RegionInfo, Long> entry : reports.entrySet()) { 236 if (tn.equals(entry.getKey().getTable())) { 237 numReports++; 238 } 239 } 240 return numReports; 241 } 242}