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.util.Map;
026import java.util.Map.Entry;
027import java.util.concurrent.atomic.AtomicLong;
028import java.util.concurrent.atomic.AtomicReference;
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.Waiter.Predicate;
034import org.apache.hadoop.hbase.client.Connection;
035import org.apache.hadoop.hbase.client.RegionInfo;
036import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
037import org.apache.hadoop.hbase.master.HMaster;
038import org.apache.hadoop.hbase.quotas.SpaceQuotaSnapshot.SpaceQuotaStatus;
039import org.apache.hadoop.hbase.quotas.policies.MissingSnapshotViolationPolicyEnforcement;
040import org.apache.hadoop.hbase.regionserver.HRegionServer;
041import org.apache.hadoop.hbase.testclassification.MediumTests;
042import org.junit.jupiter.api.AfterAll;
043import org.junit.jupiter.api.BeforeAll;
044import org.junit.jupiter.api.BeforeEach;
045import org.junit.jupiter.api.Tag;
046import org.junit.jupiter.api.Test;
047import org.junit.jupiter.api.TestInfo;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051/**
052 * Test class for the quota status RPCs in the master and regionserver.
053 */
054@Tag(MediumTests.TAG)
055public class TestQuotaStatusRPCs {
056
057  private static final Logger LOG = LoggerFactory.getLogger(TestQuotaStatusRPCs.class);
058  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
059  private static final AtomicLong COUNTER = new AtomicLong(0);
060
061  private SpaceQuotaHelperForTests helper;
062
063  @BeforeAll
064  public static 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    TEST_UTIL.startMiniCluster(1);
069  }
070
071  @AfterAll
072  public static void tearDown() throws Exception {
073    TEST_UTIL.shutdownMiniCluster();
074  }
075
076  @BeforeEach
077  public void setupForTest(TestInfo testInfo) throws Exception {
078    helper = new SpaceQuotaHelperForTests(TEST_UTIL, () -> testInfo.getTestMethod().get().getName(),
079      COUNTER);
080  }
081
082  @Test
083  public void testRegionSizesFromMaster() throws Exception {
084    final long tableSize = 1024L * 10L; // 10KB
085    final int numRegions = 10;
086    final TableName tn = helper.createTableWithRegions(numRegions);
087    // Will write at least `tableSize` data
088    helper.writeData(tn, tableSize);
089
090    final HMaster master = TEST_UTIL.getMiniHBaseCluster().getMaster();
091    final MasterQuotaManager quotaManager = master.getMasterQuotaManager();
092    // Make sure the master has all of the reports
093    Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000, new Predicate<Exception>() {
094      @Override
095      public boolean evaluate() throws Exception {
096        Map<RegionInfo, Long> regionSizes = quotaManager.snapshotRegionSizes();
097        LOG.trace("Region sizes=" + regionSizes);
098        return numRegions == countRegionsForTable(tn, regionSizes)
099          && tableSize <= getTableSize(tn, regionSizes);
100      }
101    });
102
103    Map<TableName, Long> sizes = TEST_UTIL.getAdmin().getSpaceQuotaTableSizes();
104    Long size = sizes.get(tn);
105    assertNotNull(size, "No reported size for " + tn);
106    assertTrue(size.longValue() >= tableSize, "Reported table size was " + size);
107  }
108
109  @Test
110  public void testQuotaSnapshotsFromRS() throws Exception {
111    final long sizeLimit = 1024L * 1024L; // 1MB
112    final long tableSize = 1024L * 10L; // 10KB
113    final int numRegions = 10;
114    final TableName tn = helper.createTableWithRegions(numRegions);
115
116    // Define the quota
117    QuotaSettings settings =
118      QuotaSettingsFactory.limitTableSpace(tn, sizeLimit, SpaceViolationPolicy.NO_INSERTS);
119    TEST_UTIL.getAdmin().setQuota(settings);
120
121    // Write at least `tableSize` data
122    helper.writeData(tn, tableSize);
123
124    final HRegionServer rs = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0);
125    final RegionServerSpaceQuotaManager manager = rs.getRegionServerSpaceQuotaManager();
126    Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000, new Predicate<Exception>() {
127      @Override
128      public boolean evaluate() throws Exception {
129        SpaceQuotaSnapshot snapshot = manager.copyQuotaSnapshots().get(tn);
130        if (snapshot == null) {
131          return false;
132        }
133        return snapshot.getUsage() >= tableSize;
134      }
135    });
136
137    @SuppressWarnings("unchecked")
138    Map<TableName, SpaceQuotaSnapshot> snapshots = (Map<TableName, SpaceQuotaSnapshot>) TEST_UTIL
139      .getAdmin().getRegionServerSpaceQuotaSnapshots(rs.getServerName());
140    SpaceQuotaSnapshot snapshot = snapshots.get(tn);
141    assertNotNull(snapshot, "Did not find snapshot for " + tn);
142    assertTrue(snapshot.getUsage() >= tableSize, "Observed table usage was " + snapshot.getUsage());
143    assertEquals(sizeLimit, snapshot.getLimit());
144    SpaceQuotaStatus pbStatus = snapshot.getQuotaStatus();
145    assertFalse(pbStatus.isInViolation());
146  }
147
148  @Test
149  public void testQuotaEnforcementsFromRS() throws Exception {
150    final long sizeLimit = 1024L * 8L; // 8KB
151    final long tableSize = 1024L * 10L; // 10KB
152    final int numRegions = 10;
153    final TableName tn = helper.createTableWithRegions(numRegions);
154
155    // Define the quota
156    QuotaSettings settings =
157      QuotaSettingsFactory.limitTableSpace(tn, sizeLimit, SpaceViolationPolicy.NO_INSERTS);
158    TEST_UTIL.getAdmin().setQuota(settings);
159
160    // Write at least `tableSize` data
161    try {
162      helper.writeData(tn, tableSize);
163    } catch (RetriesExhaustedWithDetailsException | SpaceLimitingException e) {
164      // Pass
165    }
166
167    final HRegionServer rs = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0);
168    final RegionServerSpaceQuotaManager manager = rs.getRegionServerSpaceQuotaManager();
169    Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000, new Predicate<Exception>() {
170      @Override
171      public boolean evaluate() throws Exception {
172        ActivePolicyEnforcement enforcements = manager.getActiveEnforcements();
173        SpaceViolationPolicyEnforcement enforcement = enforcements.getPolicyEnforcement(tn);
174        // Signifies that we're waiting on the quota snapshot to be fetched
175        if (enforcement instanceof MissingSnapshotViolationPolicyEnforcement) {
176          return false;
177        }
178        return enforcement.getQuotaSnapshot().getQuotaStatus().isInViolation();
179      }
180    });
181
182    // We obtain the violations for a RegionServer by observing the snapshots
183    @SuppressWarnings("unchecked")
184    Map<TableName, SpaceQuotaSnapshot> snapshots = (Map<TableName, SpaceQuotaSnapshot>) TEST_UTIL
185      .getAdmin().getRegionServerSpaceQuotaSnapshots(rs.getServerName());
186    SpaceQuotaSnapshot snapshot = snapshots.get(tn);
187    assertNotNull(snapshot, "Did not find snapshot for " + tn);
188    assertTrue(snapshot.getQuotaStatus().isInViolation());
189    assertEquals(SpaceViolationPolicy.NO_INSERTS, snapshot.getQuotaStatus().getPolicy().get());
190  }
191
192  @Test
193  public void testQuotaStatusFromMaster() throws Exception {
194    final long sizeLimit = 1024L * 25L; // 25KB
195    // As of 2.0.0-beta-2, this 1KB of "Cells" actually results in about 15KB on disk (HFiles)
196    // This is skewed a bit since we're writing such little data, so the test needs to keep
197    // this in mind; else, the quota will be in violation before the test expects it to be.
198    final long tableSize = 1024L * 1; // 1KB
199    final long nsLimit = Long.MAX_VALUE;
200    final int numRegions = 10;
201    final TableName tn = helper.createTableWithRegions(numRegions);
202
203    // Define the quota
204    QuotaSettings settings =
205      QuotaSettingsFactory.limitTableSpace(tn, sizeLimit, SpaceViolationPolicy.NO_INSERTS);
206    TEST_UTIL.getAdmin().setQuota(settings);
207    QuotaSettings nsSettings = QuotaSettingsFactory.limitNamespaceSpace(tn.getNamespaceAsString(),
208      nsLimit, SpaceViolationPolicy.NO_INSERTS);
209    TEST_UTIL.getAdmin().setQuota(nsSettings);
210
211    // Write at least `tableSize` data
212    helper.writeData(tn, tableSize);
213
214    final Connection conn = TEST_UTIL.getConnection();
215    // Make sure the master has a snapshot for our table
216    Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000, new Predicate<Exception>() {
217      @Override
218      public boolean evaluate() throws Exception {
219        SpaceQuotaSnapshot snapshot =
220          (SpaceQuotaSnapshot) conn.getAdmin().getCurrentSpaceQuotaSnapshot(tn);
221        LOG.info("Table snapshot after initial ingest: " + snapshot);
222        if (snapshot == null) {
223          return false;
224        }
225        return snapshot.getLimit() == sizeLimit && snapshot.getUsage() > 0L;
226      }
227    });
228    final AtomicReference<Long> nsUsage = new AtomicReference<>();
229    // If we saw the table snapshot, we should also see the namespace snapshot
230    Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000 * 1000, new Predicate<Exception>() {
231      @Override
232      public boolean evaluate() throws Exception {
233        SpaceQuotaSnapshot snapshot = (SpaceQuotaSnapshot) conn.getAdmin()
234          .getCurrentSpaceQuotaSnapshot(tn.getNamespaceAsString());
235        LOG.debug("Namespace snapshot after initial ingest: " + snapshot);
236        if (snapshot == null) {
237          return false;
238        }
239        nsUsage.set(snapshot.getUsage());
240        return snapshot.getLimit() == nsLimit && snapshot.getUsage() > 0;
241      }
242    });
243
244    // Sanity check: the below assertions will fail if we somehow write too much data
245    // and force the table to move into violation before we write the second bit of data.
246    SpaceQuotaSnapshot snapshot =
247      (SpaceQuotaSnapshot) conn.getAdmin().getCurrentSpaceQuotaSnapshot(tn);
248    assertTrue(snapshot != null && !snapshot.getQuotaStatus().isInViolation(),
249      "QuotaSnapshot for " + tn + " should be non-null and not in violation");
250
251    try {
252      helper.writeData(tn, tableSize * 2L);
253    } catch (RetriesExhaustedWithDetailsException | SpaceLimitingException e) {
254      // Pass
255    }
256
257    // Wait for the status to move to violation
258    Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000, new Predicate<Exception>() {
259      @Override
260      public boolean evaluate() throws Exception {
261        SpaceQuotaSnapshot snapshot =
262          (SpaceQuotaSnapshot) conn.getAdmin().getCurrentSpaceQuotaSnapshot(tn);
263        LOG.info("Table snapshot after second ingest: " + snapshot);
264        if (snapshot == null) {
265          return false;
266        }
267        return snapshot.getQuotaStatus().isInViolation();
268      }
269    });
270    // The namespace should still not be in violation, but have a larger usage than previously
271    Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000, new Predicate<Exception>() {
272      @Override
273      public boolean evaluate() throws Exception {
274        SpaceQuotaSnapshot snapshot = (SpaceQuotaSnapshot) conn.getAdmin()
275          .getCurrentSpaceQuotaSnapshot(tn.getNamespaceAsString());
276        LOG.debug("Namespace snapshot after second ingest: " + snapshot);
277        if (snapshot == null) {
278          return false;
279        }
280        return snapshot.getUsage() > nsUsage.get() && !snapshot.getQuotaStatus().isInViolation();
281      }
282    });
283  }
284
285  private int countRegionsForTable(TableName tn, Map<RegionInfo, Long> regionSizes) {
286    int size = 0;
287    for (RegionInfo regionInfo : regionSizes.keySet()) {
288      if (tn.equals(regionInfo.getTable())) {
289        size++;
290      }
291    }
292    return size;
293  }
294
295  private int getTableSize(TableName tn, Map<RegionInfo, Long> regionSizes) {
296    int tableSize = 0;
297    for (Entry<RegionInfo, Long> entry : regionSizes.entrySet()) {
298      RegionInfo regionInfo = entry.getKey();
299      long regionSize = entry.getValue();
300      if (tn.equals(regionInfo.getTable())) {
301        tableSize += regionSize;
302      }
303    }
304    return tableSize;
305  }
306}