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   public void onRegionMerged(HRegionInfo hri) throws IOException {
337     if (enabled) {
338       namespaceQuotaManager.updateQuotaForRegionMerge(hri);
339     }
340   }
341 
342   public void onRegionSplit(HRegionInfo hri) throws IOException {
343     if (enabled) {
344       namespaceQuotaManager.checkQuotaToSplitRegion(hri);
345     }
346   }
347 
348   /**
349    * Remove table from namespace quota.
350    * @param tName - The table name to update quota usage.
351    * @throws IOException Signals that an I/O exception has occurred.
352    */
353   public void removeTableFromNamespaceQuota(TableName tName) throws IOException {
354     if (enabled) {
355       namespaceQuotaManager.removeFromNamespaceUsage(tName);
356     }
357   }
358 
359   public NamespaceAuditor getNamespaceQuotaManager() {
360     return this.namespaceQuotaManager;
361   }
362 
363   private static interface SetQuotaOperations {
364     Quotas fetch() throws IOException;
365 
366     void delete() throws IOException;
367 
368     void update(final Quotas quotas) throws IOException;
369 
370     void preApply(final Quotas quotas) throws IOException;
371 
372     void postApply(final Quotas quotas) throws IOException;
373   }
374 
375   /*
376    * ========================================================================== Helpers to apply
377    * changes to the quotas
378    */
379   private void applyThrottle(final Quotas.Builder quotas, final ThrottleRequest req)
380       throws IOException {
381     Throttle.Builder throttle;
382 
383     if (req.hasType() && (req.hasTimedQuota() || quotas.hasThrottle())) {
384       // Validate timed quota if present
385       if (req.hasTimedQuota()) {
386         validateTimedQuota(req.getTimedQuota());
387       }
388 
389       // apply the new settings
390       throttle = quotas.hasThrottle() ? quotas.getThrottle().toBuilder() : Throttle.newBuilder();
391 
392       switch (req.getType()) {
393       case REQUEST_NUMBER:
394         if (req.hasTimedQuota()) {
395           throttle.setReqNum(req.getTimedQuota());
396         } else {
397           throttle.clearReqNum();
398         }
399         break;
400       case REQUEST_SIZE:
401         if (req.hasTimedQuota()) {
402           throttle.setReqSize(req.getTimedQuota());
403         } else {
404           throttle.clearReqSize();
405         }
406         break;
407       case WRITE_NUMBER:
408         if (req.hasTimedQuota()) {
409           throttle.setWriteNum(req.getTimedQuota());
410         } else {
411           throttle.clearWriteNum();
412         }
413         break;
414       case WRITE_SIZE:
415         if (req.hasTimedQuota()) {
416           throttle.setWriteSize(req.getTimedQuota());
417         } else {
418           throttle.clearWriteSize();
419         }
420         break;
421       case READ_NUMBER:
422         if (req.hasTimedQuota()) {
423           throttle.setReadNum(req.getTimedQuota());
424         } else {
425           throttle.clearReqNum();
426         }
427         break;
428       case READ_SIZE:
429         if (req.hasTimedQuota()) {
430           throttle.setReadSize(req.getTimedQuota());
431         } else {
432           throttle.clearReadSize();
433         }
434         break;
435       default:
436         throw new RuntimeException("Invalid throttle type: " + req.getType());
437       }
438       quotas.setThrottle(throttle.build());
439     } else {
440       quotas.clearThrottle();
441     }
442   }
443 
444   private void applyBypassGlobals(final Quotas.Builder quotas, boolean bypassGlobals) {
445     if (bypassGlobals) {
446       quotas.setBypassGlobals(bypassGlobals);
447     } else {
448       quotas.clearBypassGlobals();
449     }
450   }
451 
452   private void validateTimedQuota(final TimedQuota timedQuota) throws IOException {
453     if (timedQuota.getSoftLimit() < 1) {
454       throw new DoNotRetryIOException(new UnsupportedOperationException(
455           "The throttle limit must be greater then 0, got " + timedQuota.getSoftLimit()));
456     }
457   }
458 
459   /*
460    * ========================================================================== Helpers
461    */
462 
463   private void checkQuotaSupport() throws IOException {
464     if (!enabled) {
465       throw new DoNotRetryIOException(new UnsupportedOperationException("quota support disabled"));
466     }
467   }
468 
469   private void createQuotaTable() throws IOException {
470     HRegionInfo[] newRegions = new HRegionInfo[] { new HRegionInfo(QuotaUtil.QUOTA_TABLE_NAME) };
471 
472     if (masterServices.isMasterProcedureExecutorEnabled()) {
473       masterServices.getMasterProcedureExecutor()
474         .submitProcedure(new CreateTableProcedure(
475           masterServices.getMasterProcedureExecutor().getEnvironment(),
476           QuotaUtil.QUOTA_TABLE_DESC,
477           newRegions));
478     } else {
479       masterServices.getExecutorService().submit(
480         new CreateTableHandler(masterServices, masterServices.getMasterFileSystem(),
481           QuotaUtil.QUOTA_TABLE_DESC, masterServices.getConfiguration(), newRegions,
482           masterServices).prepare());
483     }
484   }
485 
486   private static class NamedLock<T> {
487     private HashSet<T> locks = new HashSet<T>();
488 
489     public void lock(final T name) throws InterruptedException {
490       synchronized (locks) {
491         while (locks.contains(name)) {
492           locks.wait();
493         }
494         locks.add(name);
495       }
496     }
497 
498     public void unlock(final T name) {
499       synchronized (locks) {
500         locks.remove(name);
501         locks.notifyAll();
502       }
503     }
504   }
505 
506   @Override
507   public void onRegionSplitReverted(HRegionInfo hri) throws IOException {
508     if (enabled) {
509       this.namespaceQuotaManager.removeRegionFromNamespaceUsage(hri);
510     }
511   }
512 }