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.ArrayList;
16  import java.util.List;
17  import java.util.Map;
18  import java.util.Set;
19  import java.util.concurrent.ConcurrentHashMap;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.hadoop.conf.Configuration;
24  import org.apache.hadoop.hbase.ScheduledChore;
25  import org.apache.hadoop.hbase.Stoppable;
26  import org.apache.hadoop.hbase.TableName;
27  import org.apache.hadoop.hbase.classification.InterfaceAudience;
28  import org.apache.hadoop.hbase.classification.InterfaceStability;
29  import org.apache.hadoop.hbase.client.Get;
30  import org.apache.hadoop.hbase.regionserver.RegionServerServices;
31  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
32  import org.apache.hadoop.security.UserGroupInformation;
33  
34  /**
35   * Cache that keeps track of the quota settings for the users and tables that are interacting with
36   * it. To avoid blocking the operations if the requested quota is not in cache an "empty quota" will
37   * be returned and the request to fetch the quota information will be enqueued for the next refresh.
38   * TODO: At the moment the Cache has a Chore that will be triggered every 5min or on cache-miss
39   * events. Later the Quotas will be pushed using the notification system.
40   */
41  @InterfaceAudience.Private
42  @InterfaceStability.Evolving
43  public class QuotaCache implements Stoppable {
44    private static final Log LOG = LogFactory.getLog(QuotaCache.class);
45  
46    public static final String REFRESH_CONF_KEY = "hbase.quota.refresh.period";
47    private static final int REFRESH_DEFAULT_PERIOD = 5 * 60000; // 5min
48    private static final int EVICT_PERIOD_FACTOR = 5; // N * REFRESH_DEFAULT_PERIOD
49  
50    // for testing purpose only, enforce the cache to be always refreshed
51    private static boolean TEST_FORCE_REFRESH = false;
52  
53    private final ConcurrentHashMap<String, QuotaState> namespaceQuotaCache =
54        new ConcurrentHashMap<String, QuotaState>();
55    private final ConcurrentHashMap<TableName, QuotaState> tableQuotaCache =
56        new ConcurrentHashMap<TableName, QuotaState>();
57    private final ConcurrentHashMap<String, UserQuotaState> userQuotaCache =
58        new ConcurrentHashMap<String, UserQuotaState>();
59    private final RegionServerServices rsServices;
60  
61    private QuotaRefresherChore refreshChore;
62    private boolean stopped = true;
63  
64    public QuotaCache(final RegionServerServices rsServices) {
65      this.rsServices = rsServices;
66    }
67  
68    public void start() throws IOException {
69      stopped = false;
70  
71      // TODO: This will be replaced once we have the notification bus ready.
72      Configuration conf = rsServices.getConfiguration();
73      int period = conf.getInt(REFRESH_CONF_KEY, REFRESH_DEFAULT_PERIOD);
74      refreshChore = new QuotaRefresherChore(period, this);
75      rsServices.getChoreService().scheduleChore(refreshChore);
76    }
77  
78    @Override
79    public void stop(final String why) {
80      stopped = true;
81    }
82  
83    @Override
84    public boolean isStopped() {
85      return stopped;
86    }
87  
88    /**
89     * Returns the limiter associated to the specified user/table.
90     * @param ugi the user to limit
91     * @param table the table to limit
92     * @return the limiter associated to the specified user/table
93     */
94    public QuotaLimiter getUserLimiter(final UserGroupInformation ugi, final TableName table) {
95      if (table.isSystemTable()) {
96        return NoopQuotaLimiter.get();
97      }
98      return getUserQuotaState(ugi).getTableLimiter(table);
99    }
100 
101   /**
102    * Returns the QuotaState associated to the specified user.
103    * @param ugi the user
104    * @return the quota info associated to specified user
105    */
106   public UserQuotaState getUserQuotaState(final UserGroupInformation ugi) {
107     String key = ugi.getShortUserName();
108     UserQuotaState quotaInfo = userQuotaCache.get(key);
109     if (quotaInfo == null) {
110       quotaInfo = new UserQuotaState();
111       if (userQuotaCache.putIfAbsent(key, quotaInfo) == null) {
112         triggerCacheRefresh();
113       }
114     }
115     return quotaInfo;
116   }
117 
118   /**
119    * Returns the limiter associated to the specified table.
120    * @param table the table to limit
121    * @return the limiter associated to the specified table
122    */
123   public QuotaLimiter getTableLimiter(final TableName table) {
124     return getQuotaState(this.tableQuotaCache, table).getGlobalLimiter();
125   }
126 
127   /**
128    * Returns the limiter associated to the specified namespace.
129    * @param namespace the namespace to limit
130    * @return the limiter associated to the specified namespace
131    */
132   public QuotaLimiter getNamespaceLimiter(final String namespace) {
133     return getQuotaState(this.namespaceQuotaCache, namespace).getGlobalLimiter();
134   }
135 
136   /**
137    * Returns the QuotaState requested. If the quota info is not in cache an empty one will be
138    * returned and the quota request will be enqueued for the next cache refresh.
139    */
140   private <K> QuotaState
141       getQuotaState(final ConcurrentHashMap<K, QuotaState> quotasMap, final K key) {
142     QuotaState quotaInfo = quotasMap.get(key);
143     if (quotaInfo == null) {
144       quotaInfo = new QuotaState();
145       if (quotasMap.putIfAbsent(key, quotaInfo) == null) {
146         triggerCacheRefresh();
147       }
148     }
149     return quotaInfo;
150   }
151 
152   void triggerCacheRefresh() {
153     refreshChore.triggerNow();
154   }
155 
156   long getLastUpdate() {
157     return refreshChore.lastUpdate;
158   }
159 
160   Map<String, QuotaState> getNamespaceQuotaCache() {
161     return namespaceQuotaCache;
162   }
163 
164   Map<TableName, QuotaState> getTableQuotaCache() {
165     return tableQuotaCache;
166   }
167 
168   Map<String, UserQuotaState> getUserQuotaCache() {
169     return userQuotaCache;
170   }
171 
172   public static boolean isTEST_FORCE_REFRESH() {
173     return TEST_FORCE_REFRESH;
174   }
175 
176   public static void setTEST_FORCE_REFRESH(boolean tEST_FORCE_REFRESH) {
177     TEST_FORCE_REFRESH = tEST_FORCE_REFRESH;
178   }
179 
180   // TODO: Remove this once we have the notification bus
181   private class QuotaRefresherChore extends ScheduledChore {
182     private long lastUpdate = 0;
183 
184     public QuotaRefresherChore(final int period, final Stoppable stoppable) {
185       super("QuotaRefresherChore", stoppable, period);
186     }
187 
188     @Override
189     @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "GC_UNRELATED_TYPES",
190         justification = "I do not understand why the complaints, it looks good to me -- FIX")
191     protected void chore() {
192       // Prefetch online tables/namespaces
193       for (TableName table : QuotaCache.this.rsServices.getOnlineTables()) {
194         if (table.isSystemTable()) continue;
195         if (!QuotaCache.this.tableQuotaCache.containsKey(table)) {
196           QuotaCache.this.tableQuotaCache.putIfAbsent(table, new QuotaState());
197         }
198         String ns = table.getNamespaceAsString();
199         if (!QuotaCache.this.namespaceQuotaCache.containsKey(ns)) {
200           QuotaCache.this.namespaceQuotaCache.putIfAbsent(ns, new QuotaState());
201         }
202       }
203 
204       fetchNamespaceQuotaState();
205       fetchTableQuotaState();
206       fetchUserQuotaState();
207       lastUpdate = EnvironmentEdgeManager.currentTime();
208     }
209 
210     private void fetchNamespaceQuotaState() {
211       fetch("namespace", QuotaCache.this.namespaceQuotaCache, new Fetcher<String, QuotaState>() {
212         @Override
213         public Get makeGet(final Map.Entry<String, QuotaState> entry) {
214           return QuotaUtil.makeGetForNamespaceQuotas(entry.getKey());
215         }
216 
217         @Override
218         public Map<String, QuotaState> fetchEntries(final List<Get> gets) throws IOException {
219           return QuotaUtil.fetchNamespaceQuotas(rsServices.getConnection(), gets);
220         }
221       });
222     }
223 
224     private void fetchTableQuotaState() {
225       fetch("table", QuotaCache.this.tableQuotaCache, new Fetcher<TableName, QuotaState>() {
226         @Override
227         public Get makeGet(final Map.Entry<TableName, QuotaState> entry) {
228           return QuotaUtil.makeGetForTableQuotas(entry.getKey());
229         }
230 
231         @Override
232         public Map<TableName, QuotaState> fetchEntries(final List<Get> gets) throws IOException {
233           return QuotaUtil.fetchTableQuotas(rsServices.getConnection(), gets);
234         }
235       });
236     }
237 
238     private void fetchUserQuotaState() {
239       final Set<String> namespaces = QuotaCache.this.namespaceQuotaCache.keySet();
240       final Set<TableName> tables = QuotaCache.this.tableQuotaCache.keySet();
241       fetch("user", QuotaCache.this.userQuotaCache, new Fetcher<String, UserQuotaState>() {
242         @Override
243         public Get makeGet(final Map.Entry<String, UserQuotaState> entry) {
244           return QuotaUtil.makeGetForUserQuotas(entry.getKey(), tables, namespaces);
245         }
246 
247         @Override
248         public Map<String, UserQuotaState> fetchEntries(final List<Get> gets) throws IOException {
249           return QuotaUtil.fetchUserQuotas(rsServices.getConnection(), gets);
250         }
251       });
252     }
253 
254     private <K, V extends QuotaState> void fetch(final String type,
255         final ConcurrentHashMap<K, V> quotasMap, final Fetcher<K, V> fetcher) {
256       long now = EnvironmentEdgeManager.currentTime();
257       long refreshPeriod = getPeriod();
258       long evictPeriod = refreshPeriod * EVICT_PERIOD_FACTOR;
259 
260       // Find the quota entries to update
261       List<Get> gets = new ArrayList<Get>();
262       List<K> toRemove = new ArrayList<K>();
263       for (Map.Entry<K, V> entry : quotasMap.entrySet()) {
264         long lastUpdate = entry.getValue().getLastUpdate();
265         long lastQuery = entry.getValue().getLastQuery();
266         if (lastQuery > 0 && (now - lastQuery) >= evictPeriod) {
267           toRemove.add(entry.getKey());
268         } else if (isTEST_FORCE_REFRESH() || (now - lastUpdate) >= refreshPeriod) {
269           gets.add(fetcher.makeGet(entry));
270         }
271       }
272 
273       for (final K key : toRemove) {
274         if (LOG.isTraceEnabled()) {
275           LOG.trace("evict " + type + " key=" + key);
276         }
277         quotasMap.remove(key);
278       }
279 
280       // fetch and update the quota entries
281       if (!gets.isEmpty()) {
282         try {
283           for (Map.Entry<K, V> entry : fetcher.fetchEntries(gets).entrySet()) {
284             V quotaInfo = quotasMap.putIfAbsent(entry.getKey(), entry.getValue());
285             if (quotaInfo != null) {
286               quotaInfo.update(entry.getValue());
287             }
288 
289             if (LOG.isTraceEnabled()) {
290               LOG.trace("refresh " + type + " key=" + entry.getKey() + " quotas=" + quotaInfo);
291             }
292           }
293         } catch (IOException e) {
294           LOG.warn("Unable to read " + type + " from quota table", e);
295         }
296       }
297     }
298   }
299 
300   static interface Fetcher<Key, Value> {
301     Get makeGet(Map.Entry<Key, Value> entry);
302 
303     Map<Key, Value> fetchEntries(List<Get> gets) throws IOException;
304   }
305 }