View Javadoc

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