001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to you under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.hadoop.hbase.quotas;
018
019import java.io.IOException;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Map.Entry;
023import java.util.concurrent.TimeUnit;
024
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.hbase.ScheduledChore;
027import org.apache.hadoop.hbase.TableName;
028import org.apache.yetus.audience.InterfaceAudience;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031import org.apache.hadoop.hbase.client.Connection;
032import org.apache.hadoop.hbase.client.Result;
033import org.apache.hadoop.hbase.client.ResultScanner;
034import org.apache.hadoop.hbase.client.Table;
035import org.apache.hadoop.hbase.util.Bytes;
036
037/**
038 * A {@link ScheduledChore} which periodically updates the {@link RegionServerSpaceQuotaManager}
039 * with information from the hbase:quota.
040 */
041@InterfaceAudience.Private
042public class SpaceQuotaRefresherChore extends ScheduledChore {
043  private static final Logger LOG = LoggerFactory.getLogger(SpaceQuotaRefresherChore.class);
044
045  static final String POLICY_REFRESHER_CHORE_PERIOD_KEY =
046      "hbase.regionserver.quotas.policy.refresher.chore.period";
047  static final int POLICY_REFRESHER_CHORE_PERIOD_DEFAULT = 1000 * 60 * 1; // 1 minute in millis
048
049  static final String POLICY_REFRESHER_CHORE_DELAY_KEY =
050      "hbase.regionserver.quotas.policy.refresher.chore.delay";
051  static final long POLICY_REFRESHER_CHORE_DELAY_DEFAULT = 1000L * 15L; // 15 seconds in millis
052
053  static final String POLICY_REFRESHER_CHORE_TIMEUNIT_KEY =
054      "hbase.regionserver.quotas.policy.refresher.chore.timeunit";
055  static final String POLICY_REFRESHER_CHORE_TIMEUNIT_DEFAULT = TimeUnit.MILLISECONDS.name();
056
057  static final String POLICY_REFRESHER_CHORE_REPORT_PERCENT_KEY =
058      "hbase.regionserver.quotas.policy.refresher.report.percent";
059  static final double POLICY_REFRESHER_CHORE_REPORT_PERCENT_DEFAULT= 0.95;
060
061  private final RegionServerSpaceQuotaManager manager;
062  private final Connection conn;
063
064  public SpaceQuotaRefresherChore(RegionServerSpaceQuotaManager manager, Connection conn) {
065    super(SpaceQuotaRefresherChore.class.getSimpleName(),
066        manager.getRegionServerServices(),
067        getPeriod(manager.getRegionServerServices().getConfiguration()),
068        getInitialDelay(manager.getRegionServerServices().getConfiguration()),
069        getTimeUnit(manager.getRegionServerServices().getConfiguration()));
070    this.manager = manager;
071    this.conn = conn;
072  }
073
074  @Override
075  protected void chore() {
076    try {
077      if (LOG.isTraceEnabled()) {
078        LOG.trace("Reading current quota snapshots from hbase:quota.");
079      }
080      // Get the snapshots that the quota manager is currently aware of
081      final Map<TableName, SpaceQuotaSnapshot> currentSnapshots =
082          getManager().copyQuotaSnapshots();
083      // Read the new snapshots from the quota table
084      final Map<TableName, SpaceQuotaSnapshot> newSnapshots = fetchSnapshotsFromQuotaTable();
085      if (LOG.isTraceEnabled()) {
086        LOG.trace(currentSnapshots.size() + " table quota snapshots are collected, "
087            + "read " + newSnapshots.size() + " from the quota table.");
088      }
089      // Iterate over each new quota snapshot
090      for (Entry<TableName, SpaceQuotaSnapshot> entry : newSnapshots.entrySet()) {
091        final TableName tableName = entry.getKey();
092        final SpaceQuotaSnapshot newSnapshot = entry.getValue();
093        // May be null!
094        final SpaceQuotaSnapshot currentSnapshot = currentSnapshots.get(tableName);
095        if (LOG.isTraceEnabled()) {
096          LOG.trace(tableName + ": current=" + currentSnapshot + ", new=" + newSnapshot);
097        }
098        if (!newSnapshot.equals(currentSnapshot)) {
099          // We have a new snapshot. We might need to enforce it or disable the enforcement
100          if (!isInViolation(currentSnapshot) && newSnapshot.getQuotaStatus().isInViolation()) {
101            if (LOG.isTraceEnabled()) {
102              LOG.trace("Enabling " + newSnapshot + " on " + tableName);
103            }
104            getManager().enforceViolationPolicy(tableName, newSnapshot);
105          }
106          if (isInViolation(currentSnapshot) && !newSnapshot.getQuotaStatus().isInViolation()) {
107            if (LOG.isTraceEnabled()) {
108              LOG.trace("Removing quota violation policy on " + tableName);
109            }
110            getManager().disableViolationPolicyEnforcement(tableName);
111          }
112        }
113      }
114
115      // Disable violation policy for all such tables which have been removed in new snapshot
116      for (TableName tableName : currentSnapshots.keySet()) {
117        // check whether table was removed in new snapshot
118        if (!newSnapshots.containsKey(tableName)) {
119          if (LOG.isTraceEnabled()) {
120            LOG.trace("Removing quota violation policy on " + tableName);
121          }
122          getManager().disableViolationPolicyEnforcement(tableName);
123        }
124      }
125
126      // We're intentionally ignoring anything extra with the currentSnapshots. If we were missing
127      // information from the RegionServers to create an accurate SpaceQuotaSnapshot in the Master,
128      // the Master will generate a new SpaceQuotaSnapshot which represents this state. This lets
129      // us avoid having to do anything special with currentSnapshots here.
130
131      // Update the snapshots in the manager
132      getManager().updateQuotaSnapshot(newSnapshots);
133    } catch (IOException e) {
134      LOG.warn(
135          "Caught exception while refreshing enforced quota violation policies, will retry.", e);
136    }
137  }
138
139  /**
140   * Checks if the given <code>snapshot</code> is in violation, allowing the snapshot to be null.
141   * If the snapshot is null, this is interpreted as no snapshot which implies not in violation.
142   *
143   * @param snapshot The snapshot to operate on.
144   * @return true if the snapshot is in violation, false otherwise.
145   */
146  boolean isInViolation(SpaceQuotaSnapshot snapshot) {
147    if (snapshot == null) {
148      return false;
149    }
150    return snapshot.getQuotaStatus().isInViolation();
151  }
152
153  /**
154   * Reads all quota snapshots from the quota table.
155   *
156   * @return The current "view" of space use by each table.
157   */
158  public Map<TableName, SpaceQuotaSnapshot> fetchSnapshotsFromQuotaTable() throws IOException {
159    try (Table quotaTable = getConnection().getTable(QuotaUtil.QUOTA_TABLE_NAME);
160        ResultScanner scanner = quotaTable.getScanner(QuotaTableUtil.makeQuotaSnapshotScan())) {
161      Map<TableName,SpaceQuotaSnapshot> snapshots = new HashMap<>();
162      for (Result result : scanner) {
163        try {
164          extractQuotaSnapshot(result, snapshots);
165        } catch (IllegalArgumentException e) {
166          final String msg = "Failed to parse result for row " + Bytes.toString(result.getRow());
167          LOG.error(msg, e);
168          throw new IOException(msg, e);
169        }
170      }
171      return snapshots;
172    }
173  }
174
175  /**
176   * Wrapper around {@link QuotaTableUtil#extractQuotaSnapshot(Result, Map)} for testing.
177   */
178  void extractQuotaSnapshot(Result result, Map<TableName,SpaceQuotaSnapshot> snapshots) {
179    QuotaTableUtil.extractQuotaSnapshot(result, snapshots);
180  }
181
182  Connection getConnection() {
183    return conn;
184  }
185
186  RegionServerSpaceQuotaManager getManager() {
187    return manager;
188  }
189
190  /**
191   * Extracts the period for the chore from the configuration.
192   *
193   * @param conf The configuration object.
194   * @return The configured chore period or the default value.
195   */
196  static int getPeriod(Configuration conf) {
197    return conf.getInt(POLICY_REFRESHER_CHORE_PERIOD_KEY,
198        POLICY_REFRESHER_CHORE_PERIOD_DEFAULT);
199  }
200
201  /**
202   * Extracts the initial delay for the chore from the configuration.
203   *
204   * @param conf The configuration object.
205   * @return The configured chore initial delay or the default value.
206   */
207  static long getInitialDelay(Configuration conf) {
208    return conf.getLong(POLICY_REFRESHER_CHORE_DELAY_KEY,
209        POLICY_REFRESHER_CHORE_DELAY_DEFAULT);
210  }
211
212  /**
213   * Extracts the time unit for the chore period and initial delay from the configuration. The
214   * configuration value for {@link #POLICY_REFRESHER_CHORE_TIMEUNIT_KEY} must correspond to
215   * a {@link TimeUnit} value.
216   *
217   * @param conf The configuration object.
218   * @return The configured time unit for the chore period and initial delay or the default value.
219   */
220  static TimeUnit getTimeUnit(Configuration conf) {
221    return TimeUnit.valueOf(conf.get(POLICY_REFRESHER_CHORE_TIMEUNIT_KEY,
222        POLICY_REFRESHER_CHORE_TIMEUNIT_DEFAULT));
223  }
224
225  /**
226   * Extracts the percent of Regions for a table to have been reported to enable quota violation
227   * state change.
228   *
229   * @param conf The configuration object.
230   * @return The percent of regions reported to use.
231   */
232  static Double getRegionReportPercent(Configuration conf) {
233    return conf.getDouble(POLICY_REFRESHER_CHORE_REPORT_PERCENT_KEY,
234        POLICY_REFRESHER_CHORE_REPORT_PERCENT_DEFAULT);
235  }
236}