View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional information regarding
4    * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
5    * "License"); you may not use this file except in compliance with the License. You may obtain a
6    * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable
7    * law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
8    * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
9    * for the specific language governing permissions and limitations under the License.
10   */
11  
12  package org.apache.hadoop.hbase.quotas;
13  
14  import java.io.IOException;
15  import java.util.HashSet;
16  
17  import org.apache.commons.logging.Log;
18  import org.apache.commons.logging.LogFactory;
19  import org.apache.hadoop.hbase.DoNotRetryIOException;
20  import org.apache.hadoop.hbase.HRegionInfo;
21  import org.apache.hadoop.hbase.MetaTableAccessor;
22  import org.apache.hadoop.hbase.NamespaceDescriptor;
23  import org.apache.hadoop.hbase.TableName;
24  import org.apache.hadoop.hbase.classification.InterfaceAudience;
25  import org.apache.hadoop.hbase.classification.InterfaceStability;
26  import org.apache.hadoop.hbase.master.MasterServices;
27  import org.apache.hadoop.hbase.namespace.NamespaceAuditor;
28  import org.apache.hadoop.hbase.master.handler.CreateTableHandler;
29  import org.apache.hadoop.hbase.master.procedure.CreateTableProcedure;
30  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
31  import org.apache.hadoop.hbase.protobuf.generated.MasterProtos.SetQuotaRequest;
32  import org.apache.hadoop.hbase.protobuf.generated.MasterProtos.SetQuotaResponse;
33  import org.apache.hadoop.hbase.protobuf.generated.QuotaProtos.Quotas;
34  import org.apache.hadoop.hbase.protobuf.generated.QuotaProtos.Throttle;
35  import org.apache.hadoop.hbase.protobuf.generated.QuotaProtos.ThrottleRequest;
36  import org.apache.hadoop.hbase.protobuf.generated.QuotaProtos.TimedQuota;
37  
38  /**
39   * Master Quota Manager. It is responsible for initialize the quota table on the first-run and
40   * provide the admin operations to interact with the quota table. TODO: FUTURE: The master will be
41   * responsible to notify each RS of quota changes and it will do the "quota aggregation" when the
42   * QuotaScope is CLUSTER.
43   */
44  @InterfaceAudience.Private
45  @InterfaceStability.Evolving
46  public class MasterQuotaManager implements RegionStateListener {
47    private static final Log LOG = LogFactory.getLog(MasterQuotaManager.class);
48  
49    private final MasterServices masterServices;
50    private NamedLock<String> namespaceLocks;
51    private NamedLock<TableName> tableLocks;
52    private NamedLock<String> userLocks;
53    private boolean enabled = false;
54    private NamespaceAuditor namespaceQuotaManager;
55  
56    public MasterQuotaManager(final MasterServices masterServices) {
57      this.masterServices = masterServices;
58    }
59  
60    public void start() throws IOException {
61      // If the user doesn't want the quota support skip all the initializations.
62      if (!QuotaUtil.isQuotaEnabled(masterServices.getConfiguration())) {
63        LOG.info("Quota support disabled");
64        return;
65      }
66  
67      // Create the quota table if missing
68      if (!MetaTableAccessor.tableExists(masterServices.getConnection(),
69          QuotaUtil.QUOTA_TABLE_NAME)) {
70        LOG.info("Quota table not found. Creating...");
71        createQuotaTable();
72      }
73  
74      LOG.info("Initializing quota support");
75      namespaceLocks = new NamedLock<String>();
76      tableLocks = new NamedLock<TableName>();
77      userLocks = new NamedLock<String>();
78  
79      namespaceQuotaManager = new NamespaceAuditor(masterServices);
80      namespaceQuotaManager.start();
81      enabled = true;
82    }
83  
84    public void stop() {
85    }
86  
87    public boolean isQuotaEnabled() {
88      return enabled && namespaceQuotaManager.isInitialized();
89    }
90  
91    /*
92     * ========================================================================== Admin operations to
93     * manage the quota table
94     */
95    public SetQuotaResponse setQuota(final SetQuotaRequest req) throws IOException,
96        InterruptedException {
97      checkQuotaSupport();
98  
99      if (req.hasUserName()) {
100       userLocks.lock(req.getUserName());
101       try {
102         if (req.hasTableName()) {
103           setUserQuota(req.getUserName(), ProtobufUtil.toTableName(req.getTableName()), req);
104         } else if (req.hasNamespace()) {
105           setUserQuota(req.getUserName(), req.getNamespace(), req);
106         } else {
107           setUserQuota(req.getUserName(), req);
108         }
109       } finally {
110         userLocks.unlock(req.getUserName());
111       }
112     } else if (req.hasTableName()) {
113       TableName table = ProtobufUtil.toTableName(req.getTableName());
114       tableLocks.lock(table);
115       try {
116         setTableQuota(table, req);
117       } finally {
118         tableLocks.unlock(table);
119       }
120     } else if (req.hasNamespace()) {
121       namespaceLocks.lock(req.getNamespace());
122       try {
123         setNamespaceQuota(req.getNamespace(), req);
124       } finally {
125         namespaceLocks.unlock(req.getNamespace());
126       }
127     } else {
128       throw new DoNotRetryIOException(new UnsupportedOperationException(
129           "a user, a table or a namespace must be specified"));
130     }
131     return SetQuotaResponse.newBuilder().build();
132   }
133 
134   public void setUserQuota(final String userName, final SetQuotaRequest req) throws IOException,
135       InterruptedException {
136     setQuota(req, new SetQuotaOperations() {
137       @Override
138       public Quotas fetch() throws IOException {
139         return QuotaUtil.getUserQuota(masterServices.getConnection(), userName);
140       }
141 
142       @Override
143       public void update(final Quotas quotas) throws IOException {
144         QuotaUtil.addUserQuota(masterServices.getConnection(), userName, quotas);
145       }
146 
147       @Override
148       public void delete() throws IOException {
149         QuotaUtil.deleteUserQuota(masterServices.getConnection(), userName);
150       }
151 
152       @Override
153       public void preApply(final Quotas quotas) throws IOException {
154         masterServices.getMasterCoprocessorHost().preSetUserQuota(userName, quotas);
155       }
156 
157       @Override
158       public void postApply(final Quotas quotas) throws IOException {
159         masterServices.getMasterCoprocessorHost().postSetUserQuota(userName, quotas);
160       }
161     });
162   }
163 
164   public void setUserQuota(final String userName, final TableName table, final SetQuotaRequest req)
165       throws IOException, InterruptedException {
166     setQuota(req, new SetQuotaOperations() {
167       @Override
168       public Quotas fetch() throws IOException {
169         return QuotaUtil.getUserQuota(masterServices.getConnection(), userName, table);
170       }
171 
172       @Override
173       public void update(final Quotas quotas) throws IOException {
174         QuotaUtil.addUserQuota(masterServices.getConnection(), userName, table, quotas);
175       }
176 
177       @Override
178       public void delete() throws IOException {
179         QuotaUtil.deleteUserQuota(masterServices.getConnection(), userName, table);
180       }
181 
182       @Override
183       public void preApply(final Quotas quotas) throws IOException {
184         masterServices.getMasterCoprocessorHost().preSetUserQuota(userName, table, quotas);
185       }
186 
187       @Override
188       public void postApply(final Quotas quotas) throws IOException {
189         masterServices.getMasterCoprocessorHost().postSetUserQuota(userName, table, quotas);
190       }
191     });
192   }
193 
194   public void
195       setUserQuota(final String userName, final String namespace, final SetQuotaRequest req)
196           throws IOException, InterruptedException {
197     setQuota(req, new SetQuotaOperations() {
198       @Override
199       public Quotas fetch() throws IOException {
200         return QuotaUtil.getUserQuota(masterServices.getConnection(), userName, namespace);
201       }
202 
203       @Override
204       public void update(final Quotas quotas) throws IOException {
205         QuotaUtil.addUserQuota(masterServices.getConnection(), userName, namespace, quotas);
206       }
207 
208       @Override
209       public void delete() throws IOException {
210         QuotaUtil.deleteUserQuota(masterServices.getConnection(), userName, namespace);
211       }
212 
213       @Override
214       public void preApply(final Quotas quotas) throws IOException {
215         masterServices.getMasterCoprocessorHost().preSetUserQuota(userName, namespace, quotas);
216       }
217 
218       @Override
219       public void postApply(final Quotas quotas) throws IOException {
220         masterServices.getMasterCoprocessorHost().postSetUserQuota(userName, namespace, quotas);
221       }
222     });
223   }
224 
225   public void setTableQuota(final TableName table, final SetQuotaRequest req) throws IOException,
226       InterruptedException {
227     setQuota(req, new SetQuotaOperations() {
228       @Override
229       public Quotas fetch() throws IOException {
230         return QuotaUtil.getTableQuota(masterServices.getConnection(), table);
231       }
232 
233       @Override
234       public void update(final Quotas quotas) throws IOException {
235         QuotaUtil.addTableQuota(masterServices.getConnection(), table, quotas);
236       }
237 
238       @Override
239       public void delete() throws IOException {
240         QuotaUtil.deleteTableQuota(masterServices.getConnection(), table);
241       }
242 
243       @Override
244       public void preApply(final Quotas quotas) throws IOException {
245         masterServices.getMasterCoprocessorHost().preSetTableQuota(table, quotas);
246       }
247 
248       @Override
249       public void postApply(final Quotas quotas) throws IOException {
250         masterServices.getMasterCoprocessorHost().postSetTableQuota(table, quotas);
251       }
252     });
253   }
254 
255   public void setNamespaceQuota(final String namespace, final SetQuotaRequest req)
256       throws IOException, InterruptedException {
257     setQuota(req, new SetQuotaOperations() {
258       @Override
259       public Quotas fetch() throws IOException {
260         return QuotaUtil.getNamespaceQuota(masterServices.getConnection(), namespace);
261       }
262 
263       @Override
264       public void update(final Quotas quotas) throws IOException {
265         QuotaUtil.addNamespaceQuota(masterServices.getConnection(), namespace, quotas);
266       }
267 
268       @Override
269       public void delete() throws IOException {
270         QuotaUtil.deleteNamespaceQuota(masterServices.getConnection(), namespace);
271       }
272 
273       @Override
274       public void preApply(final Quotas quotas) throws IOException {
275         masterServices.getMasterCoprocessorHost().preSetNamespaceQuota(namespace, quotas);
276       }
277 
278       @Override
279       public void postApply(final Quotas quotas) throws IOException {
280         masterServices.getMasterCoprocessorHost().postSetNamespaceQuota(namespace, quotas);
281       }
282     });
283   }
284 
285   public void setNamespaceQuota(NamespaceDescriptor desc) throws IOException {
286     if (enabled) {
287       this.namespaceQuotaManager.addNamespace(desc);
288     }
289   }
290 
291   public void removeNamespaceQuota(String namespace) throws IOException {
292     if (enabled) {
293       this.namespaceQuotaManager.deleteNamespace(namespace);
294     }
295   }
296 
297   private void setQuota(final SetQuotaRequest req, final SetQuotaOperations quotaOps)
298       throws IOException, InterruptedException {
299     if (req.hasRemoveAll() && req.getRemoveAll() == true) {
300       quotaOps.preApply(null);
301       quotaOps.delete();
302       quotaOps.postApply(null);
303       return;
304     }
305 
306     // Apply quota changes
307     Quotas quotas = quotaOps.fetch();
308     quotaOps.preApply(quotas);
309 
310     Quotas.Builder builder = (quotas != null) ? quotas.toBuilder() : Quotas.newBuilder();
311     if (req.hasThrottle()) applyThrottle(builder, req.getThrottle());
312     if (req.hasBypassGlobals()) applyBypassGlobals(builder, req.getBypassGlobals());
313 
314     // Submit new changes
315     quotas = builder.build();
316     if (QuotaUtil.isEmptyQuota(quotas)) {
317       quotaOps.delete();
318     } else {
319       quotaOps.update(quotas);
320     }
321     quotaOps.postApply(quotas);
322   }
323 
324   public void checkNamespaceTableAndRegionQuota(TableName tName, int regions) throws IOException {
325     if (enabled) {
326       namespaceQuotaManager.checkQuotaToCreateTable(tName, regions);
327     }
328   }
329   
330   public void checkAndUpdateNamespaceRegionQuota(TableName tName, int regions) throws IOException {
331     if (enabled) {
332       namespaceQuotaManager.checkQuotaToUpdateRegion(tName, regions);
333     }
334   }
335 
336   /**
337    * @return cached region count, or -1 if quota manager is disabled or table status not found
338   */
339   public int getRegionCountOfTable(TableName tName) throws IOException {
340     if (enabled) {
341       return namespaceQuotaManager.getRegionCountOfTable(tName);
342     }
343     return -1;
344   }
345 
346   public void onRegionMerged(HRegionInfo hri) throws IOException {
347     if (enabled) {
348       namespaceQuotaManager.updateQuotaForRegionMerge(hri);
349     }
350   }
351 
352   public void onRegionSplit(HRegionInfo hri) throws IOException {
353     if (enabled) {
354       namespaceQuotaManager.checkQuotaToSplitRegion(hri);
355     }
356   }
357 
358   /**
359    * Remove table from namespace quota.
360    * @param tName - The table name to update quota usage.
361    * @throws IOException Signals that an I/O exception has occurred.
362    */
363   public void removeTableFromNamespaceQuota(TableName tName) throws IOException {
364     if (enabled) {
365       namespaceQuotaManager.removeFromNamespaceUsage(tName);
366     }
367   }
368 
369   public NamespaceAuditor getNamespaceQuotaManager() {
370     return this.namespaceQuotaManager;
371   }
372 
373   private static interface SetQuotaOperations {
374     Quotas fetch() throws IOException;
375 
376     void delete() throws IOException;
377 
378     void update(final Quotas quotas) throws IOException;
379 
380     void preApply(final Quotas quotas) throws IOException;
381 
382     void postApply(final Quotas quotas) throws IOException;
383   }
384 
385   /*
386    * ========================================================================== Helpers to apply
387    * changes to the quotas
388    */
389   private void applyThrottle(final Quotas.Builder quotas, final ThrottleRequest req)
390       throws IOException {
391     Throttle.Builder throttle;
392 
393     if (req.hasType() && (req.hasTimedQuota() || quotas.hasThrottle())) {
394       // Validate timed quota if present
395       if (req.hasTimedQuota()) {
396         validateTimedQuota(req.getTimedQuota());
397       }
398 
399       // apply the new settings
400       throttle = quotas.hasThrottle() ? quotas.getThrottle().toBuilder() : Throttle.newBuilder();
401 
402       switch (req.getType()) {
403       case REQUEST_NUMBER:
404         if (req.hasTimedQuota()) {
405           throttle.setReqNum(req.getTimedQuota());
406         } else {
407           throttle.clearReqNum();
408         }
409         break;
410       case REQUEST_SIZE:
411         if (req.hasTimedQuota()) {
412           throttle.setReqSize(req.getTimedQuota());
413         } else {
414           throttle.clearReqSize();
415         }
416         break;
417       case WRITE_NUMBER:
418         if (req.hasTimedQuota()) {
419           throttle.setWriteNum(req.getTimedQuota());
420         } else {
421           throttle.clearWriteNum();
422         }
423         break;
424       case WRITE_SIZE:
425         if (req.hasTimedQuota()) {
426           throttle.setWriteSize(req.getTimedQuota());
427         } else {
428           throttle.clearWriteSize();
429         }
430         break;
431       case READ_NUMBER:
432         if (req.hasTimedQuota()) {
433           throttle.setReadNum(req.getTimedQuota());
434         } else {
435           throttle.clearReqNum();
436         }
437         break;
438       case READ_SIZE:
439         if (req.hasTimedQuota()) {
440           throttle.setReadSize(req.getTimedQuota());
441         } else {
442           throttle.clearReadSize();
443         }
444         break;
445       default:
446         throw new RuntimeException("Invalid throttle type: " + req.getType());
447       }
448       quotas.setThrottle(throttle.build());
449     } else {
450       quotas.clearThrottle();
451     }
452   }
453 
454   private void applyBypassGlobals(final Quotas.Builder quotas, boolean bypassGlobals) {
455     if (bypassGlobals) {
456       quotas.setBypassGlobals(bypassGlobals);
457     } else {
458       quotas.clearBypassGlobals();
459     }
460   }
461 
462   private void validateTimedQuota(final TimedQuota timedQuota) throws IOException {
463     if (timedQuota.getSoftLimit() < 1) {
464       throw new DoNotRetryIOException(new UnsupportedOperationException(
465           "The throttle limit must be greater then 0, got " + timedQuota.getSoftLimit()));
466     }
467   }
468 
469   /*
470    * ========================================================================== Helpers
471    */
472 
473   private void checkQuotaSupport() throws IOException {
474     if (!enabled) {
475       throw new DoNotRetryIOException(new UnsupportedOperationException("quota support disabled"));
476     }
477   }
478 
479   private void createQuotaTable() throws IOException {
480     HRegionInfo[] newRegions = new HRegionInfo[] { new HRegionInfo(QuotaUtil.QUOTA_TABLE_NAME) };
481 
482     if (masterServices.isMasterProcedureExecutorEnabled()) {
483       masterServices.getMasterProcedureExecutor()
484         .submitProcedure(new CreateTableProcedure(
485           masterServices.getMasterProcedureExecutor().getEnvironment(),
486           QuotaUtil.QUOTA_TABLE_DESC,
487           newRegions));
488     } else {
489       masterServices.getExecutorService().submit(
490         new CreateTableHandler(masterServices, masterServices.getMasterFileSystem(),
491           QuotaUtil.QUOTA_TABLE_DESC, masterServices.getConfiguration(), newRegions,
492           masterServices).prepare());
493     }
494   }
495 
496   private static class NamedLock<T> {
497     private HashSet<T> locks = new HashSet<T>();
498 
499     public void lock(final T name) throws InterruptedException {
500       synchronized (locks) {
501         while (locks.contains(name)) {
502           locks.wait();
503         }
504         locks.add(name);
505       }
506     }
507 
508     public void unlock(final T name) {
509       synchronized (locks) {
510         locks.remove(name);
511         locks.notifyAll();
512       }
513     }
514   }
515 
516   @Override
517   public void onRegionSplitReverted(HRegionInfo hri) throws IOException {
518     if (enabled) {
519       this.namespaceQuotaManager.removeRegionFromNamespaceUsage(hri);
520     }
521   }
522 }