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