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.IOException;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.Map;
027import java.util.Map.Entry;
028import java.util.concurrent.ConcurrentHashMap;
029
030import org.apache.commons.lang3.builder.HashCodeBuilder;
031import org.apache.hadoop.hbase.DoNotRetryIOException;
032import org.apache.hadoop.hbase.MetaTableAccessor;
033import org.apache.hadoop.hbase.NamespaceDescriptor;
034import org.apache.hadoop.hbase.RegionStateListener;
035import org.apache.hadoop.hbase.TableName;
036import org.apache.hadoop.hbase.client.RegionInfo;
037import org.apache.hadoop.hbase.master.MasterServices;
038import org.apache.hadoop.hbase.namespace.NamespaceAuditor;
039import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
040import org.apache.yetus.audience.InterfaceAudience;
041import org.apache.yetus.audience.InterfaceStability;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
045import org.apache.hbase.thirdparty.com.google.protobuf.TextFormat;
046import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
047import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetQuotaRequest;
048import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetQuotaResponse;
049
050/**
051 * Master Quota Manager.
052 * It is responsible for initialize the quota table on the first-run and
053 * provide the admin operations to interact with the quota table.
054 *
055 * TODO: FUTURE: The master will be responsible to notify each RS of quota changes
056 * and it will do the "quota aggregation" when the QuotaScope is CLUSTER.
057 */
058@InterfaceAudience.Private
059@InterfaceStability.Evolving
060public class MasterQuotaManager implements RegionStateListener {
061  private static final Logger LOG = LoggerFactory.getLogger(MasterQuotaManager.class);
062  private static final Map<RegionInfo, Long> EMPTY_MAP = Collections.unmodifiableMap(
063      new HashMap<>());
064
065  private final MasterServices masterServices;
066  private NamedLock<String> namespaceLocks;
067  private NamedLock<TableName> tableLocks;
068  private NamedLock<String> userLocks;
069  private boolean initialized = false;
070  private NamespaceAuditor namespaceQuotaManager;
071  private ConcurrentHashMap<RegionInfo, SizeSnapshotWithTimestamp> regionSizes;
072
073  public MasterQuotaManager(final MasterServices masterServices) {
074    this.masterServices = masterServices;
075  }
076
077  public void start() throws IOException {
078    // If the user doesn't want the quota support skip all the initializations.
079    if (!QuotaUtil.isQuotaEnabled(masterServices.getConfiguration())) {
080      LOG.info("Quota support disabled");
081      return;
082    }
083
084    // Create the quota table if missing
085    if (!MetaTableAccessor.tableExists(masterServices.getConnection(),
086          QuotaUtil.QUOTA_TABLE_NAME)) {
087      LOG.info("Quota table not found. Creating...");
088      createQuotaTable();
089    }
090
091    LOG.info("Initializing quota support");
092    namespaceLocks = new NamedLock<>();
093    tableLocks = new NamedLock<>();
094    userLocks = new NamedLock<>();
095    regionSizes = new ConcurrentHashMap<>();
096
097    namespaceQuotaManager = new NamespaceAuditor(masterServices);
098    namespaceQuotaManager.start();
099    initialized = true;
100  }
101
102  public void stop() {
103  }
104
105  public boolean isQuotaInitialized() {
106    return initialized && namespaceQuotaManager.isInitialized();
107  }
108
109  /* ==========================================================================
110   *  Admin operations to manage the quota table
111   */
112  public SetQuotaResponse setQuota(final SetQuotaRequest req)
113      throws IOException, InterruptedException {
114    checkQuotaSupport();
115
116    if (req.hasUserName()) {
117      userLocks.lock(req.getUserName());
118      try {
119        if (req.hasTableName()) {
120          setUserQuota(req.getUserName(), ProtobufUtil.toTableName(req.getTableName()), req);
121        } else if (req.hasNamespace()) {
122          setUserQuota(req.getUserName(), req.getNamespace(), req);
123        } else {
124          setUserQuota(req.getUserName(), req);
125        }
126      } finally {
127        userLocks.unlock(req.getUserName());
128      }
129    } else if (req.hasTableName()) {
130      TableName table = ProtobufUtil.toTableName(req.getTableName());
131      tableLocks.lock(table);
132      try {
133        setTableQuota(table, req);
134      } finally {
135        tableLocks.unlock(table);
136      }
137    } else if (req.hasNamespace()) {
138      namespaceLocks.lock(req.getNamespace());
139      try {
140        setNamespaceQuota(req.getNamespace(), req);
141      } finally {
142        namespaceLocks.unlock(req.getNamespace());
143      }
144    } else {
145      throw new DoNotRetryIOException(
146        new UnsupportedOperationException("a user, a table or a namespace must be specified"));
147    }
148    return SetQuotaResponse.newBuilder().build();
149  }
150
151  public void setUserQuota(final String userName, final SetQuotaRequest req)
152      throws IOException, InterruptedException {
153    setQuota(req, new SetQuotaOperations() {
154      @Override
155      public GlobalQuotaSettingsImpl fetch() throws IOException {
156        return new GlobalQuotaSettingsImpl(req.getUserName(), null, null, QuotaUtil.getUserQuota(
157            masterServices.getConnection(), userName));
158      }
159      @Override
160      public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
161        QuotaUtil.addUserQuota(masterServices.getConnection(), userName, quotaPojo.toQuotas());
162      }
163      @Override
164      public void delete() throws IOException {
165        QuotaUtil.deleteUserQuota(masterServices.getConnection(), userName);
166      }
167      @Override
168      public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
169        masterServices.getMasterCoprocessorHost().preSetUserQuota(userName, quotaPojo);
170      }
171      @Override
172      public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
173        masterServices.getMasterCoprocessorHost().postSetUserQuota(userName, quotaPojo);
174      }
175    });
176  }
177
178  public void setUserQuota(final String userName, final TableName table,
179      final SetQuotaRequest req) throws IOException, InterruptedException {
180    setQuota(req, new SetQuotaOperations() {
181      @Override
182      public GlobalQuotaSettingsImpl fetch() throws IOException {
183        return new GlobalQuotaSettingsImpl(userName, table, null, QuotaUtil.getUserQuota(
184            masterServices.getConnection(), userName, table));
185      }
186      @Override
187      public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
188        QuotaUtil.addUserQuota(masterServices.getConnection(), userName, table,
189            quotaPojo.toQuotas());
190      }
191      @Override
192      public void delete() throws IOException {
193        QuotaUtil.deleteUserQuota(masterServices.getConnection(), userName, table);
194      }
195      @Override
196      public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
197        masterServices.getMasterCoprocessorHost().preSetUserQuota(userName, table, quotaPojo);
198      }
199      @Override
200      public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
201        masterServices.getMasterCoprocessorHost().postSetUserQuota(userName, table, quotaPojo);
202      }
203    });
204  }
205
206  public void setUserQuota(final String userName, final String namespace,
207      final SetQuotaRequest req) throws IOException, InterruptedException {
208    setQuota(req, new SetQuotaOperations() {
209      @Override
210      public GlobalQuotaSettingsImpl fetch() throws IOException {
211        return new GlobalQuotaSettingsImpl(userName, null, namespace, QuotaUtil.getUserQuota(
212            masterServices.getConnection(), userName, namespace));
213      }
214      @Override
215      public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
216        QuotaUtil.addUserQuota(masterServices.getConnection(), userName, namespace,
217            quotaPojo.toQuotas());
218      }
219      @Override
220      public void delete() throws IOException {
221        QuotaUtil.deleteUserQuota(masterServices.getConnection(), userName, namespace);
222      }
223      @Override
224      public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
225        masterServices.getMasterCoprocessorHost().preSetUserQuota(
226            userName, namespace, quotaPojo);
227      }
228      @Override
229      public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
230        masterServices.getMasterCoprocessorHost().postSetUserQuota(
231            userName, namespace, quotaPojo);
232      }
233    });
234  }
235
236  public void setTableQuota(final TableName table, final SetQuotaRequest req)
237      throws IOException, InterruptedException {
238    setQuota(req, new SetQuotaOperations() {
239      @Override
240      public GlobalQuotaSettingsImpl fetch() throws IOException {
241        return new GlobalQuotaSettingsImpl(null, table, null, QuotaUtil.getTableQuota(
242            masterServices.getConnection(), table));
243      }
244      @Override
245      public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
246        QuotaUtil.addTableQuota(masterServices.getConnection(), table, quotaPojo.toQuotas());
247      }
248      @Override
249      public void delete() throws IOException {
250        QuotaUtil.deleteTableQuota(masterServices.getConnection(), table);
251      }
252      @Override
253      public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
254        masterServices.getMasterCoprocessorHost().preSetTableQuota(table, quotaPojo);
255      }
256      @Override
257      public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
258        masterServices.getMasterCoprocessorHost().postSetTableQuota(table, quotaPojo);
259      }
260    });
261  }
262
263  public void setNamespaceQuota(final String namespace, final SetQuotaRequest req)
264      throws IOException, InterruptedException {
265    setQuota(req, new SetQuotaOperations() {
266      @Override
267      public GlobalQuotaSettingsImpl fetch() throws IOException {
268        return new GlobalQuotaSettingsImpl(null, null, namespace, QuotaUtil.getNamespaceQuota(
269                masterServices.getConnection(), namespace));
270      }
271      @Override
272      public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
273        QuotaUtil.addNamespaceQuota(masterServices.getConnection(), namespace,
274            ((GlobalQuotaSettingsImpl) quotaPojo).toQuotas());
275      }
276      @Override
277      public void delete() throws IOException {
278        QuotaUtil.deleteNamespaceQuota(masterServices.getConnection(), namespace);
279      }
280      @Override
281      public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
282        masterServices.getMasterCoprocessorHost().preSetNamespaceQuota(namespace, quotaPojo);
283      }
284      @Override
285      public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException {
286        masterServices.getMasterCoprocessorHost().postSetNamespaceQuota(namespace, quotaPojo);
287      }
288    });
289  }
290
291  public void setNamespaceQuota(NamespaceDescriptor desc) throws IOException {
292    if (initialized) {
293      this.namespaceQuotaManager.addNamespace(desc);
294    }
295  }
296
297  public void removeNamespaceQuota(String namespace) throws IOException {
298    if (initialized) {
299      this.namespaceQuotaManager.deleteNamespace(namespace);
300    }
301  }
302
303  private void setQuota(final SetQuotaRequest req, final SetQuotaOperations quotaOps)
304      throws IOException, InterruptedException {
305    if (req.hasRemoveAll() && req.getRemoveAll() == true) {
306      quotaOps.preApply(null);
307      quotaOps.delete();
308      quotaOps.postApply(null);
309      return;
310    }
311
312    // Apply quota changes
313    GlobalQuotaSettingsImpl currentQuota = quotaOps.fetch();
314    if (LOG.isTraceEnabled()) {
315      LOG.trace(
316          "Current quota for request(" + TextFormat.shortDebugString(req)
317              + "): " + currentQuota);
318    }
319    // Call the appropriate "pre" CP hook with the current quota value (may be null)
320    quotaOps.preApply(currentQuota);
321    // Translate the protobuf request back into a POJO
322    QuotaSettings newQuota = QuotaSettings.buildFromProto(req);
323    if (LOG.isTraceEnabled()) {
324      LOG.trace("Deserialized quota from request: " + newQuota);
325    }
326
327    // Merge the current quota settings with the new quota settings the user provided.
328    //
329    // NB: while SetQuotaRequest technically allows for multi types of quotas to be set in one
330    // message, the Java API (in Admin/AsyncAdmin) does not. Assume there is only one type.
331    GlobalQuotaSettingsImpl mergedQuota = currentQuota.merge(newQuota);
332    if (LOG.isTraceEnabled()) {
333      LOG.trace("Computed merged quota from current quota and user request: " + mergedQuota);
334    }
335
336    // Submit new changes
337    if (mergedQuota == null) {
338      quotaOps.delete();
339    } else {
340      quotaOps.update(mergedQuota);
341    }
342    // Advertise the final result via the "post" CP hook
343    quotaOps.postApply(mergedQuota);
344  }
345
346  public void checkNamespaceTableAndRegionQuota(TableName tName, int regions) throws IOException {
347    if (initialized) {
348      namespaceQuotaManager.checkQuotaToCreateTable(tName, regions);
349    }
350  }
351
352  public void checkAndUpdateNamespaceRegionQuota(TableName tName, int regions) throws IOException {
353    if (initialized) {
354      namespaceQuotaManager.checkQuotaToUpdateRegion(tName, regions);
355    }
356  }
357
358  /**
359   * @return cached region count, or -1 if quota manager is disabled or table status not found
360  */
361  public int getRegionCountOfTable(TableName tName) throws IOException {
362    if (initialized) {
363      return namespaceQuotaManager.getRegionCountOfTable(tName);
364    }
365    return -1;
366  }
367
368  @Override
369  public void onRegionMerged(RegionInfo mergedRegion) throws IOException {
370    if (initialized) {
371      namespaceQuotaManager.updateQuotaForRegionMerge(mergedRegion);
372    }
373  }
374
375  @Override
376  public void onRegionSplit(RegionInfo hri) throws IOException {
377    if (initialized) {
378      namespaceQuotaManager.checkQuotaToSplitRegion(hri);
379    }
380  }
381
382  /**
383   * Remove table from namespace quota.
384   *
385   * @param tName - The table name to update quota usage.
386   * @throws IOException Signals that an I/O exception has occurred.
387   */
388  public void removeTableFromNamespaceQuota(TableName tName) throws IOException {
389    if (initialized) {
390      namespaceQuotaManager.removeFromNamespaceUsage(tName);
391    }
392  }
393
394  public NamespaceAuditor getNamespaceQuotaManager() {
395    return this.namespaceQuotaManager;
396  }
397
398  /**
399   * Encapsulates CRUD quota operations for some subject.
400   */
401  private static interface SetQuotaOperations {
402    /**
403     * Fetches the current quota settings for the subject.
404     */
405    GlobalQuotaSettingsImpl fetch() throws IOException;
406    /**
407     * Deletes the quota for the subject.
408     */
409    void delete() throws IOException;
410    /**
411     * Persist the given quota for the subject.
412     */
413    void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException;
414    /**
415     * Performs some action before {@link #update(GlobalQuotaSettingsImpl)} with the current
416     * quota for the subject.
417     */
418    void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException;
419    /**
420     * Performs some action after {@link #update(GlobalQuotaSettingsImpl)} with the resulting
421     * quota from the request action for the subject.
422     */
423    void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException;
424  }
425
426  /* ==========================================================================
427   *  Helpers
428   */
429
430  private void checkQuotaSupport() throws IOException {
431    if (!QuotaUtil.isQuotaEnabled(masterServices.getConfiguration())) {
432      throw new DoNotRetryIOException(
433        new UnsupportedOperationException("quota support disabled"));
434    }
435    if (!initialized) {
436      long maxWaitTime = masterServices.getConfiguration().getLong(
437        "hbase.master.wait.for.quota.manager.init", 30000); // default is 30 seconds.
438      long startTime = EnvironmentEdgeManager.currentTime();
439      do {
440        try {
441          Thread.sleep(100);
442        } catch (InterruptedException e) {
443          LOG.warn("Interrupted while waiting for Quota Manager to be initialized.");
444          break;
445        }
446      } while (!initialized && (EnvironmentEdgeManager.currentTime() - startTime) < maxWaitTime);
447      if (!initialized) {
448        throw new IOException("Quota manager is uninitialized, please retry later.");
449      }
450    }
451  }
452
453  private void createQuotaTable() throws IOException {
454    masterServices.createSystemTable(QuotaUtil.QUOTA_TABLE_DESC);
455  }
456
457  private static class NamedLock<T> {
458    private final HashSet<T> locks = new HashSet<>();
459
460    public void lock(final T name) throws InterruptedException {
461      synchronized (locks) {
462        while (locks.contains(name)) {
463          locks.wait();
464        }
465        locks.add(name);
466      }
467    }
468
469    public void unlock(final T name) {
470      synchronized (locks) {
471        locks.remove(name);
472        locks.notifyAll();
473      }
474    }
475  }
476
477  @Override
478  public void onRegionSplitReverted(RegionInfo hri) throws IOException {
479    if (initialized) {
480      this.namespaceQuotaManager.removeRegionFromNamespaceUsage(hri);
481    }
482  }
483
484  /**
485   * Holds the size of a region at the given time, millis since the epoch.
486   */
487  private static class SizeSnapshotWithTimestamp {
488    private final long size;
489    private final long time;
490
491    public SizeSnapshotWithTimestamp(long size, long time) {
492      this.size = size;
493      this.time = time;
494    }
495
496    public long getSize() {
497      return size;
498    }
499
500    public long getTime() {
501      return time;
502    }
503
504    @Override
505    public boolean equals(Object o) {
506      if (o instanceof SizeSnapshotWithTimestamp) {
507        SizeSnapshotWithTimestamp other = (SizeSnapshotWithTimestamp) o;
508        return size == other.size && time == other.time;
509      }
510      return false;
511    }
512
513    @Override
514    public int hashCode() {
515      HashCodeBuilder hcb = new HashCodeBuilder();
516      return hcb.append(size).append(time).toHashCode();
517    }
518
519    @Override
520    public String toString() {
521      StringBuilder sb = new StringBuilder(32);
522      sb.append("SizeSnapshotWithTimestamp={size=").append(size).append("B, ");
523      sb.append("time=").append(time).append("}");
524      return sb.toString();
525    }
526  }
527
528  @VisibleForTesting
529  void initializeRegionSizes() {
530    assert regionSizes == null;
531    this.regionSizes = new ConcurrentHashMap<>();
532  }
533
534  public void addRegionSize(RegionInfo hri, long size, long time) {
535    if (regionSizes == null) {
536      return;
537    }
538    regionSizes.put(hri, new SizeSnapshotWithTimestamp(size, time));
539  }
540
541  public Map<RegionInfo, Long> snapshotRegionSizes() {
542    if (regionSizes == null) {
543      return EMPTY_MAP;
544    }
545
546    Map<RegionInfo, Long> copy = new HashMap<>();
547    for (Entry<RegionInfo, SizeSnapshotWithTimestamp> entry : regionSizes.entrySet()) {
548      copy.put(entry.getKey(), entry.getValue().getSize());
549    }
550    return copy;
551  }
552
553  int pruneEntriesOlderThan(long timeToPruneBefore) {
554    if (regionSizes == null) {
555      return 0;
556    }
557    int numEntriesRemoved = 0;
558    Iterator<Entry<RegionInfo,SizeSnapshotWithTimestamp>> iterator =
559        regionSizes.entrySet().iterator();
560    while (iterator.hasNext()) {
561      long currentEntryTime = iterator.next().getValue().getTime();
562      if (currentEntryTime < timeToPruneBefore) {
563        iterator.remove();
564        numEntriesRemoved++;
565      }
566    }
567    return numEntriesRemoved;
568  }
569}
570