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