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}