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 */
018
019package org.apache.hadoop.hbase.quotas;
020
021import java.io.ByteArrayInputStream;
022import java.io.ByteArrayOutputStream;
023import java.io.IOException;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Objects;
031import java.util.Set;
032import java.util.regex.Pattern;
033
034import org.apache.commons.lang3.StringUtils;
035import org.apache.hadoop.hbase.Cell;
036import org.apache.hadoop.hbase.CellScanner;
037import org.apache.hadoop.hbase.CompareOperator;
038import org.apache.hadoop.hbase.NamespaceDescriptor;
039import org.apache.hadoop.hbase.ServerName;
040import org.apache.hadoop.hbase.TableName;
041import org.apache.yetus.audience.InterfaceAudience;
042import org.apache.yetus.audience.InterfaceStability;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045import org.apache.hadoop.hbase.client.ClusterConnection;
046import org.apache.hadoop.hbase.client.Connection;
047import org.apache.hadoop.hbase.client.Delete;
048import org.apache.hadoop.hbase.client.Get;
049import org.apache.hadoop.hbase.client.Put;
050import org.apache.hadoop.hbase.client.QuotaStatusCalls;
051import org.apache.hadoop.hbase.client.Result;
052import org.apache.hadoop.hbase.client.ResultScanner;
053import org.apache.hadoop.hbase.client.Scan;
054import org.apache.hadoop.hbase.client.Table;
055import org.apache.hadoop.hbase.filter.ColumnPrefixFilter;
056import org.apache.hadoop.hbase.filter.Filter;
057import org.apache.hadoop.hbase.filter.FilterList;
058import org.apache.hadoop.hbase.filter.QualifierFilter;
059import org.apache.hadoop.hbase.filter.RegexStringComparator;
060import org.apache.hadoop.hbase.filter.RowFilter;
061import org.apache.hadoop.hbase.protobuf.ProtobufMagic;
062import org.apache.hadoop.hbase.util.Bytes;
063import org.apache.yetus.audience.InterfaceAudience;
064import org.apache.yetus.audience.InterfaceStability;
065import org.slf4j.Logger;
066import org.slf4j.LoggerFactory;
067
068import org.apache.hbase.thirdparty.com.google.common.collect.HashMultimap;
069import org.apache.hbase.thirdparty.com.google.common.collect.Multimap;
070import org.apache.hbase.thirdparty.com.google.protobuf.ByteString;
071import org.apache.hbase.thirdparty.com.google.protobuf.InvalidProtocolBufferException;
072import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations;
073import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
074import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
075import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos;
076import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetQuotaStatesResponse;
077import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaRegionSizesResponse;
078import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaSnapshotsResponse;
079import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaSnapshotsResponse.TableQuotaSnapshot;
080import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaRegionSizesResponse.RegionSizes;
081import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Quotas;
082import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.SpaceQuota;
083import org.apache.hadoop.hbase.util.Bytes;
084
085/**
086 * Helper class to interact with the quota table.
087 * <table>
088 *   <tr><th>ROW-KEY</th><th>FAM/QUAL</th><th>DATA</th><th>DESC</th></tr>
089 *   <tr><td>n.&lt;namespace&gt;</td><td>q:s</td><td>&lt;global-quotas&gt;</td></tr>
090 *   <tr><td>n.&lt;namespace&gt;</td><td>u:p</td><td>&lt;namespace-quota policy&gt;</td></tr>
091 *   <tr><td>n.&lt;namespace&gt;</td><td>u:s</td><td>&lt;SpaceQuotaSnapshot&gt;</td>
092 *      <td>The size of all snapshots against tables in the namespace</td></tr>
093 *   <tr><td>t.&lt;table&gt;</td><td>q:s</td><td>&lt;global-quotas&gt;</td></tr>
094 *   <tr><td>t.&lt;table&gt;</td><td>u:p</td><td>&lt;table-quota policy&gt;</td></tr>
095 *   <tr><td>t.&lt;table&gt;</td><td>u:ss.&lt;snapshot name&gt;</td>
096 *      <td>&lt;SpaceQuotaSnapshot&gt;</td><td>The size of a snapshot against a table</td></tr>
097 *   <tr><td>u.&lt;user&gt;</td><td>q:s</td><td>&lt;global-quotas&gt;</td></tr>
098 *   <tr><td>u.&lt;user&gt;</td><td>q:s.&lt;table&gt;</td><td>&lt;table-quotas&gt;</td></tr>
099 *   <tr><td>u.&lt;user&gt;</td><td>q:s.&lt;ns&gt;</td><td>&lt;namespace-quotas&gt;</td></tr>
100 * </table>
101 */
102@InterfaceAudience.Private
103@InterfaceStability.Evolving
104public class QuotaTableUtil {
105  private static final Logger LOG = LoggerFactory.getLogger(QuotaTableUtil.class);
106
107  /** System table for quotas */
108  public static final TableName QUOTA_TABLE_NAME =
109      TableName.valueOf(NamespaceDescriptor.SYSTEM_NAMESPACE_NAME_STR, "quota");
110
111  protected static final byte[] QUOTA_FAMILY_INFO = Bytes.toBytes("q");
112  protected static final byte[] QUOTA_FAMILY_USAGE = Bytes.toBytes("u");
113  protected static final byte[] QUOTA_QUALIFIER_SETTINGS = Bytes.toBytes("s");
114  protected static final byte[] QUOTA_QUALIFIER_SETTINGS_PREFIX = Bytes.toBytes("s.");
115  protected static final byte[] QUOTA_QUALIFIER_POLICY = Bytes.toBytes("p");
116  protected static final byte[] QUOTA_SNAPSHOT_SIZE_QUALIFIER = Bytes.toBytes("ss");
117  protected static final String QUOTA_POLICY_COLUMN =
118      Bytes.toString(QUOTA_FAMILY_USAGE) + ":" + Bytes.toString(QUOTA_QUALIFIER_POLICY);
119  protected static final byte[] QUOTA_USER_ROW_KEY_PREFIX = Bytes.toBytes("u.");
120  protected static final byte[] QUOTA_TABLE_ROW_KEY_PREFIX = Bytes.toBytes("t.");
121  protected static final byte[] QUOTA_NAMESPACE_ROW_KEY_PREFIX = Bytes.toBytes("n.");
122
123  /* =========================================================================
124   *  Quota "settings" helpers
125   */
126  public static Quotas getTableQuota(final Connection connection, final TableName table)
127      throws IOException {
128    return getQuotas(connection, getTableRowKey(table));
129  }
130
131  public static Quotas getNamespaceQuota(final Connection connection, final String namespace)
132      throws IOException {
133    return getQuotas(connection, getNamespaceRowKey(namespace));
134  }
135
136  public static Quotas getUserQuota(final Connection connection, final String user)
137      throws IOException {
138    return getQuotas(connection, getUserRowKey(user));
139  }
140
141  public static Quotas getUserQuota(final Connection connection, final String user,
142      final TableName table) throws IOException {
143    return getQuotas(connection, getUserRowKey(user), getSettingsQualifierForUserTable(table));
144  }
145
146  public static Quotas getUserQuota(final Connection connection, final String user,
147      final String namespace) throws IOException {
148    return getQuotas(connection, getUserRowKey(user),
149      getSettingsQualifierForUserNamespace(namespace));
150  }
151
152  private static Quotas getQuotas(final Connection connection, final byte[] rowKey)
153      throws IOException {
154    return getQuotas(connection, rowKey, QUOTA_QUALIFIER_SETTINGS);
155  }
156
157  private static Quotas getQuotas(final Connection connection, final byte[] rowKey,
158      final byte[] qualifier) throws IOException {
159    Get get = new Get(rowKey);
160    get.addColumn(QUOTA_FAMILY_INFO, qualifier);
161    Result result = doGet(connection, get);
162    if (result.isEmpty()) {
163      return null;
164    }
165    return quotasFromData(result.getValue(QUOTA_FAMILY_INFO, qualifier));
166  }
167
168  public static Get makeGetForTableQuotas(final TableName table) {
169    Get get = new Get(getTableRowKey(table));
170    get.addFamily(QUOTA_FAMILY_INFO);
171    return get;
172  }
173
174  public static Get makeGetForNamespaceQuotas(final String namespace) {
175    Get get = new Get(getNamespaceRowKey(namespace));
176    get.addFamily(QUOTA_FAMILY_INFO);
177    return get;
178  }
179
180  public static Get makeGetForUserQuotas(final String user, final Iterable<TableName> tables,
181      final Iterable<String> namespaces) {
182    Get get = new Get(getUserRowKey(user));
183    get.addColumn(QUOTA_FAMILY_INFO, QUOTA_QUALIFIER_SETTINGS);
184    for (final TableName table: tables) {
185      get.addColumn(QUOTA_FAMILY_INFO, getSettingsQualifierForUserTable(table));
186    }
187    for (final String ns: namespaces) {
188      get.addColumn(QUOTA_FAMILY_INFO, getSettingsQualifierForUserNamespace(ns));
189    }
190    return get;
191  }
192
193  public static Scan makeScan(final QuotaFilter filter) {
194    Scan scan = new Scan();
195    scan.addFamily(QUOTA_FAMILY_INFO);
196    if (filter != null && !filter.isNull()) {
197      scan.setFilter(makeFilter(filter));
198    }
199    return scan;
200  }
201
202  /**
203   * converts quotafilter to serializeable filterlists.
204   */
205  public static Filter makeFilter(final QuotaFilter filter) {
206    FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
207    if (StringUtils.isNotEmpty(filter.getUserFilter())) {
208      FilterList userFilters = new FilterList(FilterList.Operator.MUST_PASS_ONE);
209      boolean hasFilter = false;
210
211      if (StringUtils.isNotEmpty(filter.getNamespaceFilter())) {
212        FilterList nsFilters = new FilterList(FilterList.Operator.MUST_PASS_ALL);
213        nsFilters.addFilter(new RowFilter(CompareOperator.EQUAL,
214            new RegexStringComparator(getUserRowKeyRegex(filter.getUserFilter()), 0)));
215        nsFilters.addFilter(new QualifierFilter(CompareOperator.EQUAL,
216            new RegexStringComparator(
217              getSettingsQualifierRegexForUserNamespace(filter.getNamespaceFilter()), 0)));
218        userFilters.addFilter(nsFilters);
219        hasFilter = true;
220      }
221      if (StringUtils.isNotEmpty(filter.getTableFilter())) {
222        FilterList tableFilters = new FilterList(FilterList.Operator.MUST_PASS_ALL);
223        tableFilters.addFilter(new RowFilter(CompareOperator.EQUAL,
224            new RegexStringComparator(getUserRowKeyRegex(filter.getUserFilter()), 0)));
225        tableFilters.addFilter(new QualifierFilter(CompareOperator.EQUAL,
226            new RegexStringComparator(
227              getSettingsQualifierRegexForUserTable(filter.getTableFilter()), 0)));
228        userFilters.addFilter(tableFilters);
229        hasFilter = true;
230      }
231      if (!hasFilter) {
232        userFilters.addFilter(new RowFilter(CompareOperator.EQUAL,
233            new RegexStringComparator(getUserRowKeyRegex(filter.getUserFilter()), 0)));
234      }
235
236      filterList.addFilter(userFilters);
237    } else if (StringUtils.isNotEmpty(filter.getTableFilter())) {
238      filterList.addFilter(new RowFilter(CompareOperator.EQUAL,
239          new RegexStringComparator(getTableRowKeyRegex(filter.getTableFilter()), 0)));
240    } else if (StringUtils.isNotEmpty(filter.getNamespaceFilter())) {
241      filterList.addFilter(new RowFilter(CompareOperator.EQUAL,
242          new RegexStringComparator(getNamespaceRowKeyRegex(filter.getNamespaceFilter()), 0)));
243    }
244    return filterList;
245  }
246
247  /**
248   * Creates a {@link Scan} which returns only quota snapshots from the quota table.
249   */
250  public static Scan makeQuotaSnapshotScan() {
251    return makeQuotaSnapshotScanForTable(null);
252  }
253
254  /**
255   * Fetches all {@link SpaceQuotaSnapshot} objects from the {@code hbase:quota} table.
256   *
257   * @param conn The HBase connection
258   * @return A map of table names and their computed snapshot.
259   */
260  public static Map<TableName,SpaceQuotaSnapshot> getSnapshots(Connection conn) throws IOException {
261    Map<TableName,SpaceQuotaSnapshot> snapshots = new HashMap<>();
262    try (Table quotaTable = conn.getTable(QUOTA_TABLE_NAME);
263        ResultScanner rs = quotaTable.getScanner(makeQuotaSnapshotScan())) {
264      for (Result r : rs) {
265        extractQuotaSnapshot(r, snapshots);
266      }
267    }
268    return snapshots;
269  }
270
271  /**
272   * Creates a {@link Scan} which returns only {@link SpaceQuotaSnapshot} from the quota table for a
273   * specific table.
274   * @param tn Optionally, a table name to limit the scan's rowkey space. Can be null.
275   */
276  public static Scan makeQuotaSnapshotScanForTable(TableName tn) {
277    Scan s = new Scan();
278    // Limit to "u:v" column
279    s.addColumn(QUOTA_FAMILY_USAGE, QUOTA_QUALIFIER_POLICY);
280    if (null == tn) {
281      s.setRowPrefixFilter(QUOTA_TABLE_ROW_KEY_PREFIX);
282    } else {
283      byte[] row = getTableRowKey(tn);
284      // Limit rowspace to the "t:" prefix
285      s.withStartRow(row, true).withStopRow(row, true);
286    }
287    return s;
288  }
289
290  /**
291   * Creates a {@link Get} which returns only {@link SpaceQuotaSnapshot} from the quota table for a
292   * specific table.
293   * @param tn table name to get from. Can't be null.
294   */
295  public static Get makeQuotaSnapshotGetForTable(TableName tn) {
296    Get g = new Get(getTableRowKey(tn));
297    // Limit to "u:v" column
298    g.addColumn(QUOTA_FAMILY_USAGE, QUOTA_QUALIFIER_POLICY);
299    return g;
300  }
301
302  /**
303   * Extracts the {@link SpaceViolationPolicy} and {@link TableName} from the provided
304   * {@link Result} and adds them to the given {@link Map}. If the result does not contain
305   * the expected information or the serialized policy in the value is invalid, this method
306   * will throw an {@link IllegalArgumentException}.
307   *
308   * @param result A row from the quota table.
309   * @param snapshots A map of snapshots to add the result of this method into.
310   */
311  public static void extractQuotaSnapshot(
312      Result result, Map<TableName,SpaceQuotaSnapshot> snapshots) {
313    byte[] row = Objects.requireNonNull(result).getRow();
314    if (row == null || row.length == 0) {
315      throw new IllegalArgumentException("Provided result had a null row");
316    }
317    final TableName targetTableName = getTableFromRowKey(row);
318    Cell c = result.getColumnLatestCell(QUOTA_FAMILY_USAGE, QUOTA_QUALIFIER_POLICY);
319    if (c == null) {
320      throw new IllegalArgumentException("Result did not contain the expected column "
321          + QUOTA_POLICY_COLUMN + ", " + result.toString());
322    }
323    ByteString buffer = UnsafeByteOperations.unsafeWrap(
324        c.getValueArray(), c.getValueOffset(), c.getValueLength());
325    try {
326      QuotaProtos.SpaceQuotaSnapshot snapshot = QuotaProtos.SpaceQuotaSnapshot.parseFrom(buffer);
327      snapshots.put(targetTableName, SpaceQuotaSnapshot.toSpaceQuotaSnapshot(snapshot));
328    } catch (InvalidProtocolBufferException e) {
329      throw new IllegalArgumentException(
330          "Result did not contain a valid SpaceQuota protocol buffer message", e);
331    }
332  }
333
334  public static interface UserQuotasVisitor {
335    void visitUserQuotas(final String userName, final Quotas quotas)
336      throws IOException;
337    void visitUserQuotas(final String userName, final TableName table, final Quotas quotas)
338      throws IOException;
339    void visitUserQuotas(final String userName, final String namespace, final Quotas quotas)
340      throws IOException;
341  }
342
343  public static interface TableQuotasVisitor {
344    void visitTableQuotas(final TableName tableName, final Quotas quotas)
345      throws IOException;
346  }
347
348  public static interface NamespaceQuotasVisitor {
349    void visitNamespaceQuotas(final String namespace, final Quotas quotas)
350      throws IOException;
351  }
352
353  public static interface QuotasVisitor extends UserQuotasVisitor,
354      TableQuotasVisitor, NamespaceQuotasVisitor {
355  }
356
357  public static void parseResult(final Result result, final QuotasVisitor visitor)
358      throws IOException {
359    byte[] row = result.getRow();
360    if (isNamespaceRowKey(row)) {
361      parseNamespaceResult(result, visitor);
362    } else if (isTableRowKey(row)) {
363      parseTableResult(result, visitor);
364    } else if (isUserRowKey(row)) {
365      parseUserResult(result, visitor);
366    } else {
367      LOG.warn("unexpected row-key: " + Bytes.toString(row));
368    }
369  }
370
371  public static void parseResultToCollection(final Result result,
372      Collection<QuotaSettings> quotaSettings) throws IOException {
373
374    QuotaTableUtil.parseResult(result, new QuotaTableUtil.QuotasVisitor() {
375      @Override
376      public void visitUserQuotas(String userName, Quotas quotas) {
377        quotaSettings.addAll(QuotaSettingsFactory.fromUserQuotas(userName, quotas));
378      }
379
380      @Override
381      public void visitUserQuotas(String userName, TableName table, Quotas quotas) {
382        quotaSettings.addAll(QuotaSettingsFactory.fromUserQuotas(userName, table, quotas));
383      }
384
385      @Override
386      public void visitUserQuotas(String userName, String namespace, Quotas quotas) {
387        quotaSettings.addAll(QuotaSettingsFactory.fromUserQuotas(userName, namespace, quotas));
388      }
389
390      @Override
391      public void visitTableQuotas(TableName tableName, Quotas quotas) {
392        quotaSettings.addAll(QuotaSettingsFactory.fromTableQuotas(tableName, quotas));
393      }
394
395      @Override
396      public void visitNamespaceQuotas(String namespace, Quotas quotas) {
397        quotaSettings.addAll(QuotaSettingsFactory.fromNamespaceQuotas(namespace, quotas));
398      }
399    });
400  }
401
402  public static void parseNamespaceResult(final Result result,
403      final NamespaceQuotasVisitor visitor) throws IOException {
404    String namespace = getNamespaceFromRowKey(result.getRow());
405    parseNamespaceResult(namespace, result, visitor);
406  }
407
408  protected static void parseNamespaceResult(final String namespace, final Result result,
409      final NamespaceQuotasVisitor visitor) throws IOException {
410    byte[] data = result.getValue(QUOTA_FAMILY_INFO, QUOTA_QUALIFIER_SETTINGS);
411    if (data != null) {
412      Quotas quotas = quotasFromData(data);
413      visitor.visitNamespaceQuotas(namespace, quotas);
414    }
415  }
416
417  public static void parseTableResult(final Result result, final TableQuotasVisitor visitor)
418      throws IOException {
419    TableName table = getTableFromRowKey(result.getRow());
420    parseTableResult(table, result, visitor);
421  }
422
423  protected static void parseTableResult(final TableName table, final Result result,
424      final TableQuotasVisitor visitor) throws IOException {
425    byte[] data = result.getValue(QUOTA_FAMILY_INFO, QUOTA_QUALIFIER_SETTINGS);
426    if (data != null) {
427      Quotas quotas = quotasFromData(data);
428      visitor.visitTableQuotas(table, quotas);
429    }
430  }
431
432  public static void parseUserResult(final Result result, final UserQuotasVisitor visitor)
433      throws IOException {
434    String userName = getUserFromRowKey(result.getRow());
435    parseUserResult(userName, result, visitor);
436  }
437
438  protected static void parseUserResult(final String userName, final Result result,
439      final UserQuotasVisitor visitor) throws IOException {
440    Map<byte[], byte[]> familyMap = result.getFamilyMap(QUOTA_FAMILY_INFO);
441    if (familyMap == null || familyMap.isEmpty()) return;
442
443    for (Map.Entry<byte[], byte[]> entry: familyMap.entrySet()) {
444      Quotas quotas = quotasFromData(entry.getValue());
445      if (Bytes.startsWith(entry.getKey(), QUOTA_QUALIFIER_SETTINGS_PREFIX)) {
446        String name = Bytes.toString(entry.getKey(), QUOTA_QUALIFIER_SETTINGS_PREFIX.length);
447        if (name.charAt(name.length() - 1) == TableName.NAMESPACE_DELIM) {
448          String namespace = name.substring(0, name.length() - 1);
449          visitor.visitUserQuotas(userName, namespace, quotas);
450        } else {
451          TableName table = TableName.valueOf(name);
452          visitor.visitUserQuotas(userName, table, quotas);
453        }
454      } else if (Bytes.equals(entry.getKey(), QUOTA_QUALIFIER_SETTINGS)) {
455        visitor.visitUserQuotas(userName, quotas);
456      }
457    }
458  }
459
460  /**
461   * Creates a {@link Put} to store the given {@code snapshot} for the given {@code tableName} in
462   * the quota table.
463   */
464  static Put createPutForSpaceSnapshot(TableName tableName, SpaceQuotaSnapshot snapshot) {
465    Put p = new Put(getTableRowKey(tableName));
466    p.addColumn(
467        QUOTA_FAMILY_USAGE, QUOTA_QUALIFIER_POLICY,
468        SpaceQuotaSnapshot.toProtoSnapshot(snapshot).toByteArray());
469    return p;
470  }
471
472  /**
473   * Creates a {@link Get} for the HBase snapshot's size against the given table.
474   */
475  static Get makeGetForSnapshotSize(TableName tn, String snapshot) {
476    Get g = new Get(Bytes.add(QUOTA_TABLE_ROW_KEY_PREFIX, Bytes.toBytes(tn.toString())));
477    g.addColumn(
478        QUOTA_FAMILY_USAGE,
479        Bytes.add(QUOTA_SNAPSHOT_SIZE_QUALIFIER, Bytes.toBytes(snapshot)));
480    return g;
481  }
482
483  /**
484   * Creates a {@link Put} to persist the current size of the {@code snapshot} with respect to
485   * the given {@code table}.
486   */
487  static Put createPutForSnapshotSize(TableName tableName, String snapshot, long size) {
488    // We just need a pb message with some `long usage`, so we can just reuse the
489    // SpaceQuotaSnapshot message instead of creating a new one.
490    Put p = new Put(getTableRowKey(tableName));
491    p.addColumn(QUOTA_FAMILY_USAGE, getSnapshotSizeQualifier(snapshot),
492        org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.SpaceQuotaSnapshot
493            .newBuilder().setQuotaUsage(size).build().toByteArray());
494    return p;
495  }
496
497  /**
498   * Creates a {@code Put} for the namespace's total snapshot size.
499   */
500  static Put createPutForNamespaceSnapshotSize(String namespace, long size) {
501    Put p = new Put(getNamespaceRowKey(namespace));
502    p.addColumn(QUOTA_FAMILY_USAGE, QUOTA_SNAPSHOT_SIZE_QUALIFIER,
503        org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.SpaceQuotaSnapshot
504            .newBuilder().setQuotaUsage(size).build().toByteArray());
505    return p;
506  }
507
508  /**
509   * Returns a list of {@code Delete} to remove given table snapshot
510   * entries to remove from quota table
511   * @param snapshotEntriesToRemove the entries to remove
512   */
513  static List<Delete> createDeletesForExistingTableSnapshotSizes(
514      Multimap<TableName, String> snapshotEntriesToRemove) {
515    List<Delete> deletes = new ArrayList<>();
516    for (Map.Entry<TableName, Collection<String>> entry : snapshotEntriesToRemove.asMap()
517        .entrySet()) {
518      for (String snapshot : entry.getValue()) {
519        Delete d = new Delete(getTableRowKey(entry.getKey()));
520        d.addColumns(QUOTA_FAMILY_USAGE,
521            Bytes.add(QUOTA_SNAPSHOT_SIZE_QUALIFIER, Bytes.toBytes(snapshot)));
522        deletes.add(d);
523      }
524    }
525    return deletes;
526  }
527
528  /**
529   * Returns a list of {@code Delete} to remove all table snapshot entries from quota table.
530   * @param connection connection to re-use
531   */
532  static List<Delete> createDeletesForExistingTableSnapshotSizes(Connection connection)
533      throws IOException {
534    return createDeletesForExistingSnapshotsFromScan(connection, createScanForSpaceSnapshotSizes());
535  }
536
537  /**
538   * Returns a list of {@code Delete} to remove given namespace snapshot
539   * entries to removefrom quota table
540   * @param snapshotEntriesToRemove the entries to remove
541   */
542  static List<Delete> createDeletesForExistingNamespaceSnapshotSizes(
543      Set<String> snapshotEntriesToRemove) {
544    List<Delete> deletes = new ArrayList<>();
545    for (String snapshot : snapshotEntriesToRemove) {
546      Delete d = new Delete(getNamespaceRowKey(snapshot));
547      d.addColumns(QUOTA_FAMILY_USAGE, QUOTA_SNAPSHOT_SIZE_QUALIFIER);
548      deletes.add(d);
549    }
550    return deletes;
551  }
552
553  /**
554   * Returns a list of {@code Delete} to remove all namespace snapshot entries from quota table.
555   * @param connection connection to re-use
556   */
557  static List<Delete> createDeletesForExistingNamespaceSnapshotSizes(Connection connection)
558      throws IOException {
559    return createDeletesForExistingSnapshotsFromScan(connection,
560        createScanForNamespaceSnapshotSizes());
561  }
562
563  /**
564   * Returns a list of {@code Delete} to remove all entries returned by the passed scanner.
565   * @param connection connection to re-use
566   * @param scan the scanner to use to generate the list of deletes
567   */
568  static List<Delete> createDeletesForExistingSnapshotsFromScan(Connection connection, Scan scan)
569      throws IOException {
570    List<Delete> deletes = new ArrayList<>();
571    try (Table quotaTable = connection.getTable(QUOTA_TABLE_NAME);
572        ResultScanner rs = quotaTable.getScanner(scan)) {
573      for (Result r : rs) {
574        CellScanner cs = r.cellScanner();
575        while (cs.advance()) {
576          Cell c = cs.current();
577          byte[] family = Bytes.copy(c.getFamilyArray(), c.getFamilyOffset(), c.getFamilyLength());
578          byte[] qual =
579              Bytes.copy(c.getQualifierArray(), c.getQualifierOffset(), c.getQualifierLength());
580          Delete d = new Delete(r.getRow());
581          d.addColumns(family, qual);
582          deletes.add(d);
583        }
584      }
585      return deletes;
586    }
587  }
588
589  /**
590   * Fetches the computed size of all snapshots against tables in a namespace for space quotas.
591   */
592  static long getNamespaceSnapshotSize(
593      Connection conn, String namespace) throws IOException {
594    try (Table quotaTable = conn.getTable(QuotaTableUtil.QUOTA_TABLE_NAME)) {
595      Result r = quotaTable.get(createGetNamespaceSnapshotSize(namespace));
596      if (r.isEmpty()) {
597        return 0L;
598      }
599      r.advance();
600      return parseSnapshotSize(r.current());
601    } catch (InvalidProtocolBufferException e) {
602      throw new IOException("Could not parse snapshot size value for namespace " + namespace, e);
603    }
604  }
605
606  /**
607   * Creates a {@code Get} to fetch the namespace's total snapshot size.
608   */
609  static Get createGetNamespaceSnapshotSize(String namespace) {
610    Get g = new Get(getNamespaceRowKey(namespace));
611    g.addColumn(QUOTA_FAMILY_USAGE, QUOTA_SNAPSHOT_SIZE_QUALIFIER);
612    return g;
613  }
614
615  /**
616   * Parses the snapshot size from the given Cell's value.
617   */
618  static long parseSnapshotSize(Cell c) throws InvalidProtocolBufferException {
619    ByteString bs = UnsafeByteOperations.unsafeWrap(
620        c.getValueArray(), c.getValueOffset(), c.getValueLength());
621    return QuotaProtos.SpaceQuotaSnapshot.parseFrom(bs).getQuotaUsage();
622  }
623
624  /**
625   * Returns a scanner for all existing namespace snapshot entries.
626   */
627  static Scan createScanForNamespaceSnapshotSizes() {
628    return createScanForNamespaceSnapshotSizes(null);
629  }
630
631  /**
632   * Returns a scanner for all namespace snapshot entries of the given namespace
633   * @param namespace name of the namespace whose snapshot entries are to be scanned
634   */
635  static Scan createScanForNamespaceSnapshotSizes(String namespace) {
636    Scan s = new Scan();
637    if (namespace == null || namespace.isEmpty()) {
638      // Read all namespaces, just look at the row prefix
639      s.setRowPrefixFilter(QUOTA_NAMESPACE_ROW_KEY_PREFIX);
640    } else {
641      // Fetch the exact row for the table
642      byte[] rowkey = getNamespaceRowKey(namespace);
643      // Fetch just this one row
644      s.withStartRow(rowkey).withStopRow(rowkey, true);
645    }
646
647    // Just the usage family and only the snapshot size qualifiers
648    return s.addFamily(QUOTA_FAMILY_USAGE)
649        .setFilter(new ColumnPrefixFilter(QUOTA_SNAPSHOT_SIZE_QUALIFIER));
650  }
651
652  static Scan createScanForSpaceSnapshotSizes() {
653    return createScanForSpaceSnapshotSizes(null);
654  }
655
656  static Scan createScanForSpaceSnapshotSizes(TableName table) {
657    Scan s = new Scan();
658    if (null == table) {
659      // Read all tables, just look at the row prefix
660      s.setRowPrefixFilter(QUOTA_TABLE_ROW_KEY_PREFIX);
661    } else {
662      // Fetch the exact row for the table
663      byte[] rowkey = getTableRowKey(table);
664      // Fetch just this one row
665      s.withStartRow(rowkey).withStopRow(rowkey, true);
666    }
667
668    // Just the usage family and only the snapshot size qualifiers
669    return s.addFamily(QUOTA_FAMILY_USAGE).setFilter(
670        new ColumnPrefixFilter(QUOTA_SNAPSHOT_SIZE_QUALIFIER));
671  }
672
673  /**
674   * Fetches any persisted HBase snapshot sizes stored in the quota table. The sizes here are
675   * computed relative to the table which the snapshot was created from. A snapshot's size will
676   * not include the size of files which the table still refers. These sizes, in bytes, are what
677   * is used internally to compute quota violation for tables and namespaces.
678   *
679   * @return A map of snapshot name to size in bytes per space quota computations
680   */
681  public static Map<String,Long> getObservedSnapshotSizes(Connection conn) throws IOException {
682    try (Table quotaTable = conn.getTable(QUOTA_TABLE_NAME);
683        ResultScanner rs = quotaTable.getScanner(createScanForSpaceSnapshotSizes())) {
684      final Map<String,Long> snapshotSizes = new HashMap<>();
685      for (Result r : rs) {
686        CellScanner cs = r.cellScanner();
687        while (cs.advance()) {
688          Cell c = cs.current();
689          final String snapshot = extractSnapshotNameFromSizeCell(c);
690          final long size = parseSnapshotSize(c);
691          snapshotSizes.put(snapshot, size);
692        }
693      }
694      return snapshotSizes;
695    }
696  }
697
698  /**
699   * Returns a multimap for all existing table snapshot entries.
700   * @param conn connection to re-use
701   */
702  public static Multimap<TableName, String> getTableSnapshots(Connection conn) throws IOException {
703    try (Table quotaTable = conn.getTable(QUOTA_TABLE_NAME);
704        ResultScanner rs = quotaTable.getScanner(createScanForSpaceSnapshotSizes())) {
705      Multimap<TableName, String> snapshots = HashMultimap.create();
706      for (Result r : rs) {
707        CellScanner cs = r.cellScanner();
708        while (cs.advance()) {
709          Cell c = cs.current();
710
711          final String snapshot = extractSnapshotNameFromSizeCell(c);
712          snapshots.put(getTableFromRowKey(r.getRow()), snapshot);
713        }
714      }
715      return snapshots;
716    }
717  }
718
719  /**
720   * Returns a set of the names of all namespaces containing snapshot entries.
721   * @param conn connection to re-use
722   */
723  public static Set<String> getNamespaceSnapshots(Connection conn) throws IOException {
724    try (Table quotaTable = conn.getTable(QUOTA_TABLE_NAME);
725        ResultScanner rs = quotaTable.getScanner(createScanForNamespaceSnapshotSizes())) {
726      Set<String> snapshots = new HashSet<>();
727      for (Result r : rs) {
728        CellScanner cs = r.cellScanner();
729        while (cs.advance()) {
730          cs.current();
731          snapshots.add(getNamespaceFromRowKey(r.getRow()));
732        }
733      }
734      return snapshots;
735    }
736  }
737
738  /* =========================================================================
739   *  Space quota status RPC helpers
740   */
741  /**
742   * Fetches the table sizes on the filesystem as tracked by the HBase Master.
743   */
744  public static Map<TableName,Long> getMasterReportedTableSizes(
745      Connection conn) throws IOException {
746    if (!(conn instanceof ClusterConnection)) {
747      throw new IllegalArgumentException("Expected a ClusterConnection");
748    }
749    ClusterConnection clusterConn = (ClusterConnection) conn;
750    GetSpaceQuotaRegionSizesResponse response = QuotaStatusCalls.getMasterRegionSizes(
751        clusterConn, 0);
752    Map<TableName,Long> tableSizes = new HashMap<>();
753    for (RegionSizes sizes : response.getSizesList()) {
754      TableName tn = ProtobufUtil.toTableName(sizes.getTableName());
755      tableSizes.put(tn, sizes.getSize());
756    }
757    return tableSizes;
758  }
759
760  /**
761   * Fetches the observed {@link SpaceQuotaSnapshot}s observed by a RegionServer.
762   */
763  public static Map<TableName,SpaceQuotaSnapshot> getRegionServerQuotaSnapshots(
764      Connection conn, ServerName regionServer) throws IOException {
765    if (!(conn instanceof ClusterConnection)) {
766      throw new IllegalArgumentException("Expected a ClusterConnection");
767    }
768    ClusterConnection clusterConn = (ClusterConnection) conn;
769    GetSpaceQuotaSnapshotsResponse response = QuotaStatusCalls.getRegionServerQuotaSnapshot(
770        clusterConn, 0, regionServer);
771    Map<TableName,SpaceQuotaSnapshot> snapshots = new HashMap<>();
772    for (TableQuotaSnapshot snapshot : response.getSnapshotsList()) {
773      snapshots.put(
774          ProtobufUtil.toTableName(snapshot.getTableName()),
775          SpaceQuotaSnapshot.toSpaceQuotaSnapshot(snapshot.getSnapshot()));
776    }
777    return snapshots;
778  }
779
780  /**
781   * Returns the Master's view of a quota on the given {@code tableName} or null if the
782   * Master has no quota information on that table.
783   */
784  public static SpaceQuotaSnapshot getCurrentSnapshot(
785      Connection conn, TableName tn) throws IOException {
786    if (!(conn instanceof ClusterConnection)) {
787      throw new IllegalArgumentException("Expected a ClusterConnection");
788    }
789    ClusterConnection clusterConn = (ClusterConnection) conn;
790    GetQuotaStatesResponse resp = QuotaStatusCalls.getMasterQuotaStates(clusterConn, 0);
791    HBaseProtos.TableName protoTableName = ProtobufUtil.toProtoTableName(tn);
792    for (GetQuotaStatesResponse.TableQuotaSnapshot tableSnapshot : resp.getTableSnapshotsList()) {
793      if (protoTableName.equals(tableSnapshot.getTableName())) {
794        return SpaceQuotaSnapshot.toSpaceQuotaSnapshot(tableSnapshot.getSnapshot());
795      }
796    }
797    return null;
798  }
799
800  /**
801   * Returns the Master's view of a quota on the given {@code namespace} or null if the
802   * Master has no quota information on that namespace.
803   */
804  public static SpaceQuotaSnapshot getCurrentSnapshot(
805      Connection conn, String namespace) throws IOException {
806    if (!(conn instanceof ClusterConnection)) {
807      throw new IllegalArgumentException("Expected a ClusterConnection");
808    }
809    ClusterConnection clusterConn = (ClusterConnection) conn;
810    GetQuotaStatesResponse resp = QuotaStatusCalls.getMasterQuotaStates(clusterConn, 0);
811    for (GetQuotaStatesResponse.NamespaceQuotaSnapshot nsSnapshot : resp.getNsSnapshotsList()) {
812      if (namespace.equals(nsSnapshot.getNamespace())) {
813        return SpaceQuotaSnapshot.toSpaceQuotaSnapshot(nsSnapshot.getSnapshot());
814      }
815    }
816    return null;
817  }
818
819  /**
820   * Returns the current space quota snapshot of the given {@code tableName} from
821   * {@code QuotaTableUtil.QUOTA_TABLE_NAME} or null if the no quota information is available for
822   * that tableName.
823   * @param conn connection to re-use
824   * @param tableName name of the table whose current snapshot is to be retreived
825   */
826  public static SpaceQuotaSnapshot getCurrentSnapshotFromQuotaTable(Connection conn,
827      TableName tableName) throws IOException {
828    try (Table quotaTable = conn.getTable(QuotaTableUtil.QUOTA_TABLE_NAME)) {
829      Map<TableName, SpaceQuotaSnapshot> snapshots = new HashMap<>(1);
830      Result result = quotaTable.get(makeQuotaSnapshotGetForTable(tableName));
831      // if we don't have any row corresponding to this get, return null
832      if (result.isEmpty()) {
833        return null;
834      }
835      // otherwise, extract quota snapshot in snapshots object
836      extractQuotaSnapshot(result, snapshots);
837      return snapshots.get(tableName);
838    }
839  }
840
841  /* =========================================================================
842   *  Quotas protobuf helpers
843   */
844  protected static Quotas quotasFromData(final byte[] data) throws IOException {
845    return quotasFromData(data, 0, data.length);
846  }
847
848  protected static Quotas quotasFromData(
849      final byte[] data, int offset, int length) throws IOException {
850    int magicLen = ProtobufMagic.lengthOfPBMagic();
851    if (!ProtobufMagic.isPBMagicPrefix(data, offset, magicLen)) {
852      throw new IOException("Missing pb magic prefix");
853    }
854    return Quotas.parseFrom(new ByteArrayInputStream(data, offset + magicLen, length - magicLen));
855  }
856
857  protected static byte[] quotasToData(final Quotas data) throws IOException {
858    ByteArrayOutputStream stream = new ByteArrayOutputStream();
859    stream.write(ProtobufMagic.PB_MAGIC);
860    data.writeTo(stream);
861    return stream.toByteArray();
862  }
863
864  public static boolean isEmptyQuota(final Quotas quotas) {
865    boolean hasSettings = false;
866    hasSettings |= quotas.hasThrottle();
867    hasSettings |= quotas.hasBypassGlobals();
868    // Only when there is a space quota, make sure there's actually both fields provided
869    // Otherwise, it's a noop.
870    if (quotas.hasSpace()) {
871      hasSettings |= (quotas.getSpace().hasSoftLimit() && quotas.getSpace().hasViolationPolicy());
872    }
873    return !hasSettings;
874  }
875
876  /* =========================================================================
877   *  HTable helpers
878   */
879  protected static Result doGet(final Connection connection, final Get get)
880      throws IOException {
881    try (Table table = connection.getTable(QUOTA_TABLE_NAME)) {
882      return table.get(get);
883    }
884  }
885
886  protected static Result[] doGet(final Connection connection, final List<Get> gets)
887      throws IOException {
888    try (Table table = connection.getTable(QUOTA_TABLE_NAME)) {
889      return table.get(gets);
890    }
891  }
892
893  /* =========================================================================
894   *  Quota table row key helpers
895   */
896  protected static byte[] getUserRowKey(final String user) {
897    return Bytes.add(QUOTA_USER_ROW_KEY_PREFIX, Bytes.toBytes(user));
898  }
899
900  protected static byte[] getTableRowKey(final TableName table) {
901    return Bytes.add(QUOTA_TABLE_ROW_KEY_PREFIX, table.getName());
902  }
903
904  protected static byte[] getNamespaceRowKey(final String namespace) {
905    return Bytes.add(QUOTA_NAMESPACE_ROW_KEY_PREFIX, Bytes.toBytes(namespace));
906  }
907
908  protected static byte[] getSettingsQualifierForUserTable(final TableName tableName) {
909    return Bytes.add(QUOTA_QUALIFIER_SETTINGS_PREFIX, tableName.getName());
910  }
911
912  protected static byte[] getSettingsQualifierForUserNamespace(final String namespace) {
913    return Bytes.add(QUOTA_QUALIFIER_SETTINGS_PREFIX,
914        Bytes.toBytes(namespace + TableName.NAMESPACE_DELIM));
915  }
916
917  protected static String getUserRowKeyRegex(final String user) {
918    return getRowKeyRegEx(QUOTA_USER_ROW_KEY_PREFIX, user);
919  }
920
921  protected static String getTableRowKeyRegex(final String table) {
922    return getRowKeyRegEx(QUOTA_TABLE_ROW_KEY_PREFIX, table);
923  }
924
925  protected static String getNamespaceRowKeyRegex(final String namespace) {
926    return getRowKeyRegEx(QUOTA_NAMESPACE_ROW_KEY_PREFIX, namespace);
927  }
928
929  private static String getRowKeyRegEx(final byte[] prefix, final String regex) {
930    return '^' + Pattern.quote(Bytes.toString(prefix)) + regex + '$';
931  }
932
933  protected static String getSettingsQualifierRegexForUserTable(final String table) {
934    return '^' + Pattern.quote(Bytes.toString(QUOTA_QUALIFIER_SETTINGS_PREFIX)) +
935          table + "(?<!" + Pattern.quote(Character.toString(TableName.NAMESPACE_DELIM)) + ")$";
936  }
937
938  protected static String getSettingsQualifierRegexForUserNamespace(final String namespace) {
939    return '^' + Pattern.quote(Bytes.toString(QUOTA_QUALIFIER_SETTINGS_PREFIX)) +
940                  namespace + Pattern.quote(Character.toString(TableName.NAMESPACE_DELIM)) + '$';
941  }
942
943  protected static boolean isNamespaceRowKey(final byte[] key) {
944    return Bytes.startsWith(key, QUOTA_NAMESPACE_ROW_KEY_PREFIX);
945  }
946
947  protected static String getNamespaceFromRowKey(final byte[] key) {
948    return Bytes.toString(key, QUOTA_NAMESPACE_ROW_KEY_PREFIX.length);
949  }
950
951  protected static boolean isTableRowKey(final byte[] key) {
952    return Bytes.startsWith(key, QUOTA_TABLE_ROW_KEY_PREFIX);
953  }
954
955  protected static TableName getTableFromRowKey(final byte[] key) {
956    return TableName.valueOf(Bytes.toString(key, QUOTA_TABLE_ROW_KEY_PREFIX.length));
957  }
958
959  protected static boolean isUserRowKey(final byte[] key) {
960    return Bytes.startsWith(key, QUOTA_USER_ROW_KEY_PREFIX);
961  }
962
963  protected static String getUserFromRowKey(final byte[] key) {
964    return Bytes.toString(key, QUOTA_USER_ROW_KEY_PREFIX.length);
965  }
966
967  protected static SpaceQuota getProtoViolationPolicy(SpaceViolationPolicy policy) {
968    return SpaceQuota.newBuilder()
969          .setViolationPolicy(ProtobufUtil.toProtoViolationPolicy(policy))
970          .build();
971  }
972
973  protected static SpaceViolationPolicy getViolationPolicy(SpaceQuota proto) {
974    if (!proto.hasViolationPolicy()) {
975      throw new IllegalArgumentException("Protobuf SpaceQuota does not have violation policy.");
976    }
977    return ProtobufUtil.toViolationPolicy(proto.getViolationPolicy());
978  }
979
980  protected static byte[] getSnapshotSizeQualifier(String snapshotName) {
981    return Bytes.add(QUOTA_SNAPSHOT_SIZE_QUALIFIER, Bytes.toBytes(snapshotName));
982  }
983
984  protected static String extractSnapshotNameFromSizeCell(Cell c) {
985    return Bytes.toString(
986        c.getQualifierArray(), c.getQualifierOffset() + QUOTA_SNAPSHOT_SIZE_QUALIFIER.length,
987        c.getQualifierLength() - QUOTA_SNAPSHOT_SIZE_QUALIFIER.length);
988  }
989
990  protected static long extractSnapshotSize(
991      byte[] data, int offset, int length) throws InvalidProtocolBufferException {
992    ByteString byteStr = UnsafeByteOperations.unsafeWrap(data, offset, length);
993    return org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.SpaceQuotaSnapshot
994        .parseFrom(byteStr).getQuotaUsage();
995  }
996}