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.Collections;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Set;
026import java.util.concurrent.ConcurrentHashMap;
027import java.util.concurrent.TimeUnit;
028
029import org.apache.hadoop.conf.Configuration;
030import org.apache.hadoop.hbase.ScheduledChore;
031import org.apache.hadoop.hbase.Stoppable;
032import org.apache.hadoop.hbase.TableName;
033import org.apache.hadoop.hbase.client.Connection;
034import org.apache.hadoop.hbase.client.RegionInfo;
035import org.apache.hadoop.hbase.client.Scan;
036import org.apache.hadoop.hbase.master.HMaster;
037import org.apache.hadoop.hbase.master.MetricsMaster;
038import org.apache.hadoop.hbase.quotas.SpaceQuotaSnapshot.SpaceQuotaStatus;
039import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
040import org.apache.yetus.audience.InterfaceAudience;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
044import org.apache.hbase.thirdparty.com.google.common.collect.HashMultimap;
045import org.apache.hbase.thirdparty.com.google.common.collect.Iterables;
046import org.apache.hbase.thirdparty.com.google.common.collect.Multimap;
047import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.SpaceQuota;
048
049/**
050 * Reads the currently received Region filesystem-space use reports and acts on those which
051 * violate a defined quota.
052 */
053@InterfaceAudience.Private
054public class QuotaObserverChore extends ScheduledChore {
055  private static final Logger LOG = LoggerFactory.getLogger(QuotaObserverChore.class);
056  static final String QUOTA_OBSERVER_CHORE_PERIOD_KEY =
057      "hbase.master.quotas.observer.chore.period";
058  static final int QUOTA_OBSERVER_CHORE_PERIOD_DEFAULT = 1000 * 60 * 1; // 1 minutes in millis
059
060  static final String QUOTA_OBSERVER_CHORE_DELAY_KEY =
061      "hbase.master.quotas.observer.chore.delay";
062  static final long QUOTA_OBSERVER_CHORE_DELAY_DEFAULT = 1000L * 15L; // 15 seconds in millis
063
064  static final String QUOTA_OBSERVER_CHORE_TIMEUNIT_KEY =
065      "hbase.master.quotas.observer.chore.timeunit";
066  static final String QUOTA_OBSERVER_CHORE_TIMEUNIT_DEFAULT = TimeUnit.MILLISECONDS.name();
067
068  static final String QUOTA_OBSERVER_CHORE_REPORT_PERCENT_KEY =
069      "hbase.master.quotas.observer.report.percent";
070  static final double QUOTA_OBSERVER_CHORE_REPORT_PERCENT_DEFAULT= 0.95;
071
072  static final String REGION_REPORT_RETENTION_DURATION_KEY =
073      "hbase.master.quotas.region.report.retention.millis";
074  static final long REGION_REPORT_RETENTION_DURATION_DEFAULT =
075      1000 * 60 * 10; // 10 minutes
076
077  private final Connection conn;
078  private final Configuration conf;
079  private final MasterQuotaManager quotaManager;
080  private final MetricsMaster metrics;
081  /*
082   * Callback that changes in quota snapshots are passed to.
083   */
084  private final SpaceQuotaSnapshotNotifier snapshotNotifier;
085
086  /*
087   * Preserves the state of quota snapshots for tables and namespaces
088   */
089  private final Map<TableName,SpaceQuotaSnapshot> tableQuotaSnapshots;
090  private final Map<TableName,SpaceQuotaSnapshot> readOnlyTableQuotaSnapshots;
091  private final Map<String,SpaceQuotaSnapshot> namespaceQuotaSnapshots;
092  private final Map<String,SpaceQuotaSnapshot> readOnlyNamespaceSnapshots;
093
094  // The time, in millis, that region reports should be kept by the master
095  private final long regionReportLifetimeMillis;
096
097  /*
098   * Encapsulates logic for tracking the state of a table/namespace WRT space quotas
099   */
100  private QuotaSnapshotStore<TableName> tableSnapshotStore;
101  private QuotaSnapshotStore<String> namespaceSnapshotStore;
102
103  public QuotaObserverChore(HMaster master, MetricsMaster metrics) {
104    this(
105        master.getConnection(), master.getConfiguration(),
106        master.getSpaceQuotaSnapshotNotifier(), master.getMasterQuotaManager(),
107        master, metrics);
108  }
109
110  QuotaObserverChore(
111      Connection conn, Configuration conf, SpaceQuotaSnapshotNotifier snapshotNotifier,
112      MasterQuotaManager quotaManager, Stoppable stopper, MetricsMaster metrics) {
113    super(
114        QuotaObserverChore.class.getSimpleName(), stopper, getPeriod(conf),
115        getInitialDelay(conf), getTimeUnit(conf));
116    this.conn = conn;
117    this.conf = conf;
118    this.metrics = metrics;
119    this.quotaManager = quotaManager;
120    this.snapshotNotifier = Objects.requireNonNull(snapshotNotifier);
121    this.tableQuotaSnapshots = new ConcurrentHashMap<>();
122    this.readOnlyTableQuotaSnapshots = Collections.unmodifiableMap(tableQuotaSnapshots);
123    this.namespaceQuotaSnapshots = new ConcurrentHashMap<>();
124    this.readOnlyNamespaceSnapshots = Collections.unmodifiableMap(namespaceQuotaSnapshots);
125    this.regionReportLifetimeMillis = conf.getLong(
126        REGION_REPORT_RETENTION_DURATION_KEY, REGION_REPORT_RETENTION_DURATION_DEFAULT);
127  }
128
129  @Override
130  protected void chore() {
131    try {
132      if (LOG.isTraceEnabled()) {
133        LOG.trace("Refreshing space quotas in RegionServer");
134      }
135      long start = System.nanoTime();
136      _chore();
137      if (metrics != null) {
138        metrics.incrementQuotaObserverTime((System.nanoTime() - start) / 1_000_000);
139      }
140    } catch (IOException e) {
141      LOG.warn("Failed to process quota reports and update quota state. Will retry.", e);
142    }
143  }
144
145  void _chore() throws IOException {
146    // Get the total set of tables that have quotas defined. Includes table quotas
147    // and tables included by namespace quotas.
148    TablesWithQuotas tablesWithQuotas = fetchAllTablesWithQuotasDefined();
149    if (LOG.isTraceEnabled()) {
150      LOG.trace("Found following tables with quotas: " + tablesWithQuotas);
151    }
152
153    if (metrics != null) {
154      // Set the number of namespaces and tables with quotas defined
155      metrics.setNumSpaceQuotas(tablesWithQuotas.getTableQuotaTables().size()
156          + tablesWithQuotas.getNamespacesWithQuotas().size());
157    }
158
159    // The current "view" of region space use. Used henceforth.
160    final Map<RegionInfo,Long> reportedRegionSpaceUse = quotaManager.snapshotRegionSizes();
161    if (LOG.isTraceEnabled()) {
162      LOG.trace(
163          "Using " + reportedRegionSpaceUse.size() + " region space use reports: " +
164          reportedRegionSpaceUse);
165    }
166
167    // Remove the "old" region reports
168    pruneOldRegionReports();
169
170    // Create the stores to track table and namespace snapshots
171    initializeSnapshotStores(reportedRegionSpaceUse);
172    // Report the number of (non-expired) region size reports
173    if (metrics != null) {
174      metrics.setNumRegionSizeReports(reportedRegionSpaceUse.size());
175    }
176
177    // Filter out tables for which we don't have adequate regionspace reports yet.
178    // Important that we do this after we instantiate the stores above
179    // This gives us a set of Tables which may or may not be violating their quota.
180    // To be safe, we want to make sure that these are not in violation.
181    Set<TableName> tablesInLimbo = tablesWithQuotas.filterInsufficientlyReportedTables(
182        tableSnapshotStore);
183
184    if (LOG.isTraceEnabled()) {
185      LOG.trace("Filtered insufficiently reported tables, left with " +
186          reportedRegionSpaceUse.size() + " regions reported");
187    }
188
189    for (TableName tableInLimbo : tablesInLimbo) {
190      final SpaceQuotaSnapshot currentSnapshot = tableSnapshotStore.getCurrentState(tableInLimbo);
191      if (currentSnapshot.getQuotaStatus().isInViolation()) {
192        if (LOG.isTraceEnabled()) {
193          LOG.trace("Moving " + tableInLimbo + " out of violation because fewer region sizes were"
194              + " reported than required.");
195        }
196        SpaceQuotaSnapshot targetSnapshot = new SpaceQuotaSnapshot(
197            SpaceQuotaStatus.notInViolation(), currentSnapshot.getUsage(),
198            currentSnapshot.getLimit());
199        this.snapshotNotifier.transitionTable(tableInLimbo, targetSnapshot);
200        // Update it in the Table QuotaStore so that memory is consistent with no violation.
201        tableSnapshotStore.setCurrentState(tableInLimbo, targetSnapshot);
202      }
203    }
204
205    // Transition each table to/from quota violation based on the current and target state.
206    // Only table quotas are enacted.
207    final Set<TableName> tablesWithTableQuotas = tablesWithQuotas.getTableQuotaTables();
208    processTablesWithQuotas(tablesWithTableQuotas);
209
210    // For each Namespace quota, transition each table in the namespace in or out of violation
211    // only if a table quota violation policy has not already been applied.
212    final Set<String> namespacesWithQuotas = tablesWithQuotas.getNamespacesWithQuotas();
213    final Multimap<String,TableName> tablesByNamespace = tablesWithQuotas.getTablesByNamespace();
214    processNamespacesWithQuotas(namespacesWithQuotas, tablesByNamespace);
215  }
216
217  void initializeSnapshotStores(Map<RegionInfo,Long> regionSizes) {
218    Map<RegionInfo,Long> immutableRegionSpaceUse = Collections.unmodifiableMap(regionSizes);
219    if (tableSnapshotStore == null) {
220      tableSnapshotStore = new TableQuotaSnapshotStore(conn, this, immutableRegionSpaceUse);
221    } else {
222      tableSnapshotStore.setRegionUsage(immutableRegionSpaceUse);
223    }
224    if (namespaceSnapshotStore == null) {
225      namespaceSnapshotStore = new NamespaceQuotaSnapshotStore(
226          conn, this, immutableRegionSpaceUse);
227    } else {
228      namespaceSnapshotStore.setRegionUsage(immutableRegionSpaceUse);
229    }
230  }
231
232  /**
233   * Processes each {@code TableName} which has a quota defined and moves it in or out of
234   * violation based on the space use.
235   *
236   * @param tablesWithTableQuotas The HBase tables which have quotas defined
237   */
238  void processTablesWithQuotas(final Set<TableName> tablesWithTableQuotas) throws IOException {
239    long numTablesInViolation = 0L;
240    for (TableName table : tablesWithTableQuotas) {
241      final SpaceQuota spaceQuota = tableSnapshotStore.getSpaceQuota(table);
242      if (spaceQuota == null) {
243        if (LOG.isDebugEnabled()) {
244          LOG.debug("Unexpectedly did not find a space quota for " + table
245              + ", maybe it was recently deleted.");
246        }
247        continue;
248      }
249      final SpaceQuotaSnapshot currentSnapshot = tableSnapshotStore.getCurrentState(table);
250      final SpaceQuotaSnapshot targetSnapshot = tableSnapshotStore.getTargetState(table, spaceQuota);
251      if (LOG.isTraceEnabled()) {
252        LOG.trace("Processing " + table + " with current=" + currentSnapshot + ", target="
253            + targetSnapshot);
254      }
255      updateTableQuota(table, currentSnapshot, targetSnapshot);
256
257      if (targetSnapshot.getQuotaStatus().isInViolation()) {
258        numTablesInViolation++;
259      }
260    }
261    // Report the number of tables in violation
262    if (metrics != null) {
263      metrics.setNumTableInSpaceQuotaViolation(numTablesInViolation);
264    }
265  }
266
267  /**
268   * Processes each namespace which has a quota defined and moves all of the tables contained
269   * in that namespace into or out of violation of the quota. Tables which are already in
270   * violation of a quota at the table level which <em>also</em> have a reside in a namespace
271   * with a violated quota will not have the namespace quota enacted. The table quota takes
272   * priority over the namespace quota.
273   *
274   * @param namespacesWithQuotas The set of namespaces that have quotas defined
275   * @param tablesByNamespace A mapping of namespaces and the tables contained in those namespaces
276   */
277  void processNamespacesWithQuotas(
278      final Set<String> namespacesWithQuotas,
279      final Multimap<String,TableName> tablesByNamespace) throws IOException {
280    long numNamespacesInViolation = 0L;
281    for (String namespace : namespacesWithQuotas) {
282      // Get the quota definition for the namespace
283      final SpaceQuota spaceQuota = namespaceSnapshotStore.getSpaceQuota(namespace);
284      if (spaceQuota == null) {
285        if (LOG.isDebugEnabled()) {
286          LOG.debug("Could not get Namespace space quota for " + namespace
287              + ", maybe it was recently deleted.");
288        }
289        continue;
290      }
291      final SpaceQuotaSnapshot currentSnapshot = namespaceSnapshotStore.getCurrentState(namespace);
292      final SpaceQuotaSnapshot targetSnapshot = namespaceSnapshotStore.getTargetState(
293          namespace, spaceQuota);
294      if (LOG.isTraceEnabled()) {
295        LOG.trace("Processing " + namespace + " with current=" + currentSnapshot + ", target="
296            + targetSnapshot);
297      }
298      updateNamespaceQuota(namespace, currentSnapshot, targetSnapshot, tablesByNamespace);
299
300      if (targetSnapshot.getQuotaStatus().isInViolation()) {
301        numNamespacesInViolation++;
302      }
303    }
304
305    // Report the number of namespaces in violation
306    if (metrics != null) {
307      metrics.setNumNamespacesInSpaceQuotaViolation(numNamespacesInViolation);
308    }
309  }
310
311  /**
312   * Updates the hbase:quota table with the new quota policy for this <code>table</code>
313   * if necessary.
314   *
315   * @param table The table being checked
316   * @param currentSnapshot The state of the quota on this table from the previous invocation.
317   * @param targetSnapshot The state the quota should be in for this table.
318   */
319  void updateTableQuota(
320      TableName table, SpaceQuotaSnapshot currentSnapshot, SpaceQuotaSnapshot targetSnapshot)
321          throws IOException {
322    final SpaceQuotaStatus currentStatus = currentSnapshot.getQuotaStatus();
323    final SpaceQuotaStatus targetStatus = targetSnapshot.getQuotaStatus();
324
325    // If we're changing something, log it.
326    if (!currentSnapshot.equals(targetSnapshot)) {
327      // If the target is none, we're moving out of violation. Update the hbase:quota table
328      if (!targetStatus.isInViolation()) {
329        if (LOG.isDebugEnabled()) {
330          LOG.debug(table + " moving into observance of table space quota.");
331        }
332      } else if (LOG.isDebugEnabled()) {
333        // We're either moving into violation or changing violation policies
334        LOG.debug(table + " moving into violation of table space quota with policy of "
335            + targetStatus.getPolicy());
336      }
337
338      this.snapshotNotifier.transitionTable(table, targetSnapshot);
339      // Update it in memory
340      tableSnapshotStore.setCurrentState(table, targetSnapshot);
341    } else if (LOG.isTraceEnabled()) {
342      // Policies are the same, so we have nothing to do except log this. Don't need to re-update
343      // the quota table
344      if (!currentStatus.isInViolation()) {
345        LOG.trace(table + " remains in observance of quota.");
346      } else {
347        LOG.trace(table + " remains in violation of quota.");
348      }
349    }
350  }
351
352  /**
353   * Updates the hbase:quota table with the target quota policy for this <code>namespace</code>
354   * if necessary.
355   *
356   * @param namespace The namespace being checked
357   * @param currentSnapshot The state of the quota on this namespace from the previous invocation
358   * @param targetSnapshot The state the quota should be in for this namespace
359   * @param tablesByNamespace A mapping of tables in namespaces.
360   */
361  void updateNamespaceQuota(
362      String namespace, SpaceQuotaSnapshot currentSnapshot, SpaceQuotaSnapshot targetSnapshot,
363      final Multimap<String,TableName> tablesByNamespace) throws IOException {
364    final SpaceQuotaStatus targetStatus = targetSnapshot.getQuotaStatus();
365
366    // When the policies differ, we need to move into or out of violatino
367    if (!currentSnapshot.equals(targetSnapshot)) {
368      // We want to have a policy of "NONE", moving out of violation
369      if (!targetStatus.isInViolation()) {
370        for (TableName tableInNS : tablesByNamespace.get(namespace)) {
371          // If there is a quota on this table in violation
372          if (tableSnapshotStore.getCurrentState(tableInNS).getQuotaStatus().isInViolation()) {
373            // Table-level quota violation policy is being applied here.
374            if (LOG.isTraceEnabled()) {
375              LOG.trace("Not activating Namespace violation policy because a Table violation"
376                  + " policy is already in effect for " + tableInNS);
377            }
378          } else {
379            LOG.info(tableInNS + " moving into observance of namespace space quota");
380            this.snapshotNotifier.transitionTable(tableInNS, targetSnapshot);
381          }
382        }
383      // We want to move into violation at the NS level
384      } else {
385        // Moving tables in the namespace into violation or to a different violation policy
386        for (TableName tableInNS : tablesByNamespace.get(namespace)) {
387          final SpaceQuotaSnapshot tableQuotaSnapshot =
388                tableSnapshotStore.getCurrentState(tableInNS);
389          final boolean hasTableQuota =
390              !Objects.equals(QuotaSnapshotStore.NO_QUOTA, tableQuotaSnapshot);
391          if (hasTableQuota && tableQuotaSnapshot.getQuotaStatus().isInViolation()) {
392            // Table-level quota violation policy is being applied here.
393            if (LOG.isTraceEnabled()) {
394              LOG.trace("Not activating Namespace violation policy because a Table violation"
395                  + " policy is already in effect for " + tableInNS);
396            }
397          } else {
398            // No table quota present or a table quota present that is not in violation
399            LOG.info(tableInNS + " moving into violation of namespace space quota with policy "
400                + targetStatus.getPolicy());
401            this.snapshotNotifier.transitionTable(tableInNS, targetSnapshot);
402          }
403        }
404      }
405      // Update the new state in memory for this namespace
406      namespaceSnapshotStore.setCurrentState(namespace, targetSnapshot);
407    } else {
408      // Policies are the same
409      if (!targetStatus.isInViolation()) {
410        // Both are NONE, so we remain in observance
411        if (LOG.isTraceEnabled()) {
412          LOG.trace(namespace + " remains in observance of quota.");
413        }
414      } else {
415        // Namespace quota is still in violation, need to enact if the table quota is not
416        // taking priority.
417        for (TableName tableInNS : tablesByNamespace.get(namespace)) {
418          // Does a table policy exist
419          if (tableSnapshotStore.getCurrentState(tableInNS).getQuotaStatus().isInViolation()) {
420            // Table-level quota violation policy is being applied here.
421            if (LOG.isTraceEnabled()) {
422              LOG.trace("Not activating Namespace violation policy because Table violation"
423                  + " policy is already in effect for " + tableInNS);
424            }
425          } else {
426            // No table policy, so enact namespace policy
427            LOG.info(tableInNS + " moving into violation of namespace space quota");
428            this.snapshotNotifier.transitionTable(tableInNS, targetSnapshot);
429          }
430        }
431      }
432    }
433  }
434
435  /**
436   * Removes region reports over a certain age.
437   */
438  void pruneOldRegionReports() {
439    final long now = EnvironmentEdgeManager.currentTime();
440    final long pruneTime = now - regionReportLifetimeMillis;
441    final int numRemoved = quotaManager.pruneEntriesOlderThan(pruneTime);
442    if (LOG.isTraceEnabled()) {
443      LOG.trace("Removed " + numRemoved + " old region size reports that were older than "
444          + pruneTime + ".");
445    }
446  }
447
448  /**
449   * Computes the set of all tables that have quotas defined. This includes tables with quotas
450   * explicitly set on them, in addition to tables that exist namespaces which have a quota
451   * defined.
452   */
453  TablesWithQuotas fetchAllTablesWithQuotasDefined() throws IOException {
454    final Scan scan = QuotaTableUtil.makeScan(null);
455    final TablesWithQuotas tablesWithQuotas = new TablesWithQuotas(conn, conf);
456    try (final QuotaRetriever scanner = new QuotaRetriever()) {
457      scanner.init(conn, scan);
458      for (QuotaSettings quotaSettings : scanner) {
459        // Only one of namespace and tablename should be 'null'
460        final String namespace = quotaSettings.getNamespace();
461        final TableName tableName = quotaSettings.getTableName();
462        if (QuotaType.SPACE != quotaSettings.getQuotaType()) {
463          continue;
464        }
465
466        if (namespace != null) {
467          assert tableName == null;
468          // Collect all of the tables in the namespace
469          TableName[] tablesInNS = conn.getAdmin().listTableNamesByNamespace(namespace);
470          for (TableName tableUnderNs : tablesInNS) {
471            if (LOG.isTraceEnabled()) {
472              LOG.trace("Adding " + tableUnderNs + " under " +  namespace
473                  + " as having a namespace quota");
474            }
475            tablesWithQuotas.addNamespaceQuotaTable(tableUnderNs);
476          }
477        } else {
478          assert tableName != null;
479          if (LOG.isTraceEnabled()) {
480            LOG.trace("Adding " + tableName + " as having table quota.");
481          }
482          // namespace is already null, must be a non-null tableName
483          tablesWithQuotas.addTableQuotaTable(tableName);
484        }
485      }
486      return tablesWithQuotas;
487    }
488  }
489
490  @VisibleForTesting
491  QuotaSnapshotStore<TableName> getTableSnapshotStore() {
492    return tableSnapshotStore;
493  }
494
495  @VisibleForTesting
496  QuotaSnapshotStore<String> getNamespaceSnapshotStore() {
497    return namespaceSnapshotStore;
498  }
499
500  /**
501   * Returns an unmodifiable view over the current {@link SpaceQuotaSnapshot} objects
502   * for each HBase table with a quota defined.
503   */
504  public Map<TableName,SpaceQuotaSnapshot> getTableQuotaSnapshots() {
505    return readOnlyTableQuotaSnapshots;
506  }
507
508  /**
509   * Returns an unmodifiable view over the current {@link SpaceQuotaSnapshot} objects
510   * for each HBase namespace with a quota defined.
511   */
512  public Map<String,SpaceQuotaSnapshot> getNamespaceQuotaSnapshots() {
513    return readOnlyNamespaceSnapshots;
514  }
515
516  /**
517   * Fetches the {@link SpaceQuotaSnapshot} for the given table.
518   */
519  SpaceQuotaSnapshot getTableQuotaSnapshot(TableName table) {
520    SpaceQuotaSnapshot state = this.tableQuotaSnapshots.get(table);
521    if (state == null) {
522      // No tracked state implies observance.
523      return QuotaSnapshotStore.NO_QUOTA;
524    }
525    return state;
526  }
527
528  /**
529   * Stores the quota state for the given table.
530   */
531  void setTableQuotaSnapshot(TableName table, SpaceQuotaSnapshot snapshot) {
532    this.tableQuotaSnapshots.put(table, snapshot);
533  }
534
535  /**
536   * Fetches the {@link SpaceQuotaSnapshot} for the given namespace from this chore.
537   */
538  SpaceQuotaSnapshot getNamespaceQuotaSnapshot(String namespace) {
539    SpaceQuotaSnapshot state = this.namespaceQuotaSnapshots.get(namespace);
540    if (state == null) {
541      // No tracked state implies observance.
542      return QuotaSnapshotStore.NO_QUOTA;
543    }
544    return state;
545  }
546
547  /**
548   * Stores the given {@code snapshot} for the given {@code namespace} in this chore.
549   */
550  void setNamespaceQuotaSnapshot(String namespace, SpaceQuotaSnapshot snapshot) {
551    this.namespaceQuotaSnapshots.put(namespace, snapshot);
552  }
553
554  /**
555   * Extracts the period for the chore from the configuration.
556   *
557   * @param conf The configuration object.
558   * @return The configured chore period or the default value in the given timeunit.
559   * @see #getTimeUnit(Configuration)
560   */
561  static int getPeriod(Configuration conf) {
562    return conf.getInt(QUOTA_OBSERVER_CHORE_PERIOD_KEY,
563        QUOTA_OBSERVER_CHORE_PERIOD_DEFAULT);
564  }
565
566  /**
567   * Extracts the initial delay for the chore from the configuration.
568   *
569   * @param conf The configuration object.
570   * @return The configured chore initial delay or the default value in the given timeunit.
571   * @see #getTimeUnit(Configuration)
572   */
573  static long getInitialDelay(Configuration conf) {
574    return conf.getLong(QUOTA_OBSERVER_CHORE_DELAY_KEY,
575        QUOTA_OBSERVER_CHORE_DELAY_DEFAULT);
576  }
577
578  /**
579   * Extracts the time unit for the chore period and initial delay from the configuration. The
580   * configuration value for {@link #QUOTA_OBSERVER_CHORE_TIMEUNIT_KEY} must correspond to
581   * a {@link TimeUnit} value.
582   *
583   * @param conf The configuration object.
584   * @return The configured time unit for the chore period and initial delay or the default value.
585   */
586  static TimeUnit getTimeUnit(Configuration conf) {
587    return TimeUnit.valueOf(conf.get(QUOTA_OBSERVER_CHORE_TIMEUNIT_KEY,
588        QUOTA_OBSERVER_CHORE_TIMEUNIT_DEFAULT));
589  }
590
591  /**
592   * Extracts the percent of Regions for a table to have been reported to enable quota violation
593   * state change.
594   *
595   * @param conf The configuration object.
596   * @return The percent of regions reported to use.
597   */
598  static Double getRegionReportPercent(Configuration conf) {
599    return conf.getDouble(QUOTA_OBSERVER_CHORE_REPORT_PERCENT_KEY,
600        QUOTA_OBSERVER_CHORE_REPORT_PERCENT_DEFAULT);
601  }
602
603  /**
604   * A container which encapsulates the tables that have either a table quota or are contained in a
605   * namespace which have a namespace quota.
606   */
607  static class TablesWithQuotas {
608    private final Set<TableName> tablesWithTableQuotas = new HashSet<>();
609    private final Set<TableName> tablesWithNamespaceQuotas = new HashSet<>();
610    private final Connection conn;
611    private final Configuration conf;
612
613    public TablesWithQuotas(Connection conn, Configuration conf) {
614      this.conn = Objects.requireNonNull(conn);
615      this.conf = Objects.requireNonNull(conf);
616    }
617
618    Configuration getConfiguration() {
619      return conf;
620    }
621
622    /**
623     * Adds a table with a table quota.
624     */
625    public void addTableQuotaTable(TableName tn) {
626      tablesWithTableQuotas.add(tn);
627    }
628
629    /**
630     * Adds a table with a namespace quota.
631     */
632    public void addNamespaceQuotaTable(TableName tn) {
633      tablesWithNamespaceQuotas.add(tn);
634    }
635
636    /**
637     * Returns true if the given table has a table quota.
638     */
639    public boolean hasTableQuota(TableName tn) {
640      return tablesWithTableQuotas.contains(tn);
641    }
642
643    /**
644     * Returns true if the table exists in a namespace with a namespace quota.
645     */
646    public boolean hasNamespaceQuota(TableName tn) {
647      return tablesWithNamespaceQuotas.contains(tn);
648    }
649
650    /**
651     * Returns an unmodifiable view of all tables with table quotas.
652     */
653    public Set<TableName> getTableQuotaTables() {
654      return Collections.unmodifiableSet(tablesWithTableQuotas);
655    }
656
657    /**
658     * Returns an unmodifiable view of all tables in namespaces that have
659     * namespace quotas.
660     */
661    public Set<TableName> getNamespaceQuotaTables() {
662      return Collections.unmodifiableSet(tablesWithNamespaceQuotas);
663    }
664
665    public Set<String> getNamespacesWithQuotas() {
666      Set<String> namespaces = new HashSet<>();
667      for (TableName tn : tablesWithNamespaceQuotas) {
668        namespaces.add(tn.getNamespaceAsString());
669      }
670      return namespaces;
671    }
672
673    /**
674     * Returns a view of all tables that reside in a namespace with a namespace
675     * quota, grouped by the namespace itself.
676     */
677    public Multimap<String,TableName> getTablesByNamespace() {
678      Multimap<String,TableName> tablesByNS = HashMultimap.create();
679      for (TableName tn : tablesWithNamespaceQuotas) {
680        tablesByNS.put(tn.getNamespaceAsString(), tn);
681      }
682      return tablesByNS;
683    }
684
685    /**
686     * Filters out all tables for which the Master currently doesn't have enough region space
687     * reports received from RegionServers yet.
688     */
689    public Set<TableName> filterInsufficientlyReportedTables(
690        QuotaSnapshotStore<TableName> tableStore) throws IOException {
691      final double percentRegionsReportedThreshold = getRegionReportPercent(getConfiguration());
692      Set<TableName> tablesToRemove = new HashSet<>();
693      for (TableName table : Iterables.concat(tablesWithTableQuotas, tablesWithNamespaceQuotas)) {
694        // Don't recompute a table we've already computed
695        if (tablesToRemove.contains(table)) {
696          continue;
697        }
698        final int numRegionsInTable = getNumRegions(table);
699        // If the table doesn't exist (no regions), bail out.
700        if (numRegionsInTable == 0) {
701          if (LOG.isTraceEnabled()) {
702            LOG.trace("Filtering " + table + " because no regions were reported");
703          }
704          tablesToRemove.add(table);
705          continue;
706        }
707        final int reportedRegionsInQuota = getNumReportedRegions(table, tableStore);
708        final double ratioReported = ((double) reportedRegionsInQuota) / numRegionsInTable;
709        if (ratioReported < percentRegionsReportedThreshold) {
710          if (LOG.isTraceEnabled()) {
711            LOG.trace("Filtering " + table + " because " + reportedRegionsInQuota  + " of " +
712                numRegionsInTable + " regions were reported.");
713          }
714          tablesToRemove.add(table);
715        } else if (LOG.isTraceEnabled()) {
716          LOG.trace("Retaining " + table + " because " + reportedRegionsInQuota  + " of " +
717              numRegionsInTable + " regions were reported.");
718        }
719      }
720      for (TableName tableToRemove : tablesToRemove) {
721        tablesWithTableQuotas.remove(tableToRemove);
722        tablesWithNamespaceQuotas.remove(tableToRemove);
723      }
724      return tablesToRemove;
725    }
726
727    /**
728     * Computes the total number of regions in a table.
729     */
730    int getNumRegions(TableName table) throws IOException {
731      List<RegionInfo> regions = this.conn.getAdmin().getRegions(table);
732      if (regions == null) {
733        return 0;
734      }
735      return regions.size();
736    }
737
738    /**
739     * Computes the number of regions reported for a table.
740     */
741    int getNumReportedRegions(TableName table, QuotaSnapshotStore<TableName> tableStore)
742        throws IOException {
743      return Iterables.size(tableStore.filterBySubject(table));
744    }
745
746    @Override
747    public String toString() {
748      final StringBuilder sb = new StringBuilder(32);
749      sb.append(getClass().getSimpleName())
750          .append(": tablesWithTableQuotas=")
751          .append(this.tablesWithTableQuotas)
752          .append(", tablesWithNamespaceQuotas=")
753          .append(this.tablesWithNamespaceQuotas);
754      return sb.toString();
755    }
756  }
757}