Clover icon

Magnolia Module Memcached 5.4.2

  1. Project Clover database Wed Aug 19 2015 15:04:32 CEST
  2. Package info.magnolia.cache.memcached.spy

File MemcachedWrapper.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart9.png
0% of files have more coverage

Code metrics

26
85
19
4
331
244
36
0.42
4.47
4.75
1.89
11% of code in this file is excluded from these metrics.

Classes

Class Line # Actions
MemcachedWrapper 73 75 8.1% 30 20
0.8230088482.3%
MemcachedWrapper.CacheEntriesWithKeys 93 0 - 0 0
-1.0 -
MemcachedWrapper.ValidKey 96 8 0% 4 0
1.0100%
MemcachedWrapper.EmptyUncacheableEntry 123 2 60% 2 2
0.550%
 

Contributing tests

This file is covered by 11 tests. .

Source view

1    /**
2    * This file Copyright (c) 2008-2015 Magnolia International
3    * Ltd. (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10    * This file is distributed in the hope that it will be
11    * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12    * implied warranty of MERCHANTABILITY or FITNESS FOR A
13    * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14    * Redistribution, except as permitted by whichever of the GPL
15    * or MNA you select, is prohibited.
16    *
17    * 1. For the GPL license (GPL), you can redistribute and/or
18    * modify this file under the terms of the GNU General
19    * Public License, Version 3, as published by the Free Software
20    * Foundation. You should have received a copy of the GNU
21    * General Public License, Version 3 along with this program;
22    * if not, write to the Free Software Foundation, Inc., 51
23    * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24    *
25    * 2. For the Magnolia Network Agreement (MNA), this file
26    * and the accompanying materials are made available under the
27    * terms of the MNA which accompanies this distribution, and
28    * is available at http://www.magnolia-cms.com/mna.html
29    *
30    * Any modifications to this file must keep this entire header
31    * intact.
32    *
33    */
34    package info.magnolia.cache.memcached.spy;
35   
36    import info.magnolia.cms.security.SecurityUtil;
37    import info.magnolia.context.MgnlContext;
38    import info.magnolia.module.cache.BlockingCache;
39    import info.magnolia.module.cache.CacheModule;
40    import info.magnolia.cache.concurrent.LockType;
41    import info.magnolia.cache.concurrent.ReadWriteLockSync;
42    import info.magnolia.cache.concurrent.StripedReadWriteLockSync;
43    import info.magnolia.module.cache.exception.MgnlLockTimeoutException;
44    import info.magnolia.module.cache.filter.CachedEntry;
45    import info.magnolia.module.cache.filter.UncacheableEntry;
46    import info.magnolia.module.cache.listeners.AbstractListeningCacheWrapper;
47    import info.magnolia.module.cache.mbean.CacheMonitor;
48   
49    import java.io.IOException;
50    import java.io.Serializable;
51    import java.net.SocketAddress;
52    import java.util.Collection;
53    import java.util.HashMap;
54    import java.util.Map;
55    import java.util.concurrent.ConcurrentHashMap;
56   
57    import javax.inject.Inject;
58    import javax.servlet.FilterChain;
59    import javax.servlet.ServletException;
60    import javax.servlet.http.HttpServletRequest;
61    import javax.servlet.http.HttpServletResponse;
62   
63    import org.apache.commons.lang3.StringUtils;
64    import org.slf4j.Logger;
65    import org.slf4j.LoggerFactory;
66   
67    import net.spy.memcached.MemcachedClientIF;
68    import net.spy.memcached.OperationTimeoutException;
69   
70    /**
71    * Magnolia cache wrapper for underlying Memcached implementation.
72    */
 
73    public class MemcachedWrapper extends AbstractListeningCacheWrapper implements BlockingCache {
74   
75    private static final Logger log = LoggerFactory.getLogger(MemcachedWrapper.class);
76    private final MemcachedClientIF client;
77    private final CacheMonitor cacheMonitor;
78    private final String name;
79    private volatile int blockingTimeout;
80    private StripedReadWriteLockSync cacheLockProvider = new StripedReadWriteLockSync();
81   
82    private final Map<Object, ValidKey> validKeys = new ConcurrentHashMap<>();
83   
84    /**
85    * Cache entry holding key-value pair.
86    * Exists due to limited key length of memcached server.
87    * To prevent a MD5 hash collision we have to check the original key which is stored together with cache entry.
88    * This is a map, so we could store all of the entries with the same hash. But this not yet implemented.
89    * If this will be implemented be aware:
90    * - entries can have different TTL
91    * - size limit on memcached server (by default usually 1MB)
92    */
 
93    protected static class CacheEntriesWithKeys extends HashMap<Object, Object> implements Serializable {
94    }
95   
 
96    private class ValidKey {
97    private final String key;
98    private final boolean isHash;
99   
 
100  11 toggle private ValidKey(Object originalKey) {
101  11 final String originalKeyStr = String.valueOf(originalKey);
102  11 if (originalKeyStr.length() > MemcachedClientIF.MAX_KEY_LENGTH) {
103  3 key = createHash(originalKey);
104  3 isHash = true;
105    } else {
106  8 key = StringUtils.replaceChars(originalKeyStr, " \n\r\0", "_-\\/");
107  8 isHash = false;
108    }
109    }
110   
 
111  14 toggle private boolean isHash() {
112  14 return isHash;
113    }
114   
 
115  49 toggle private String getKey() {
116  49 return key;
117    }
118    }
119   
120    /**
121    * Entry which we return if there are are more keys with the same hash.
122    */
 
123    protected class EmptyUncacheableEntry extends UncacheableEntry {
124   
 
125  1 toggle public EmptyUncacheableEntry() {
126  1 super(new CachedEntry() {
 
127  0 toggle @Override
128    public void replay(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
129  0 throw new UnsupportedOperationException();
130    }
131   
 
132    toggle @Override
133    public String getOriginalURL() {
134    return null;
135    }
136   
 
137    toggle @Override
138    public long getLastModificationTime() {
139    return 0;
140    }
141   
 
142    toggle @Override
143    public int getTimeToLiveInSeconds() {
144    throw new UnsupportedOperationException();
145    }
146    });
147    }
148    }
149   
 
150  1 toggle protected String createHash(Object originalKey) {
151  1 return SecurityUtil.getMD5Hex(String.valueOf(originalKey));
152    }
153   
 
154  18 toggle @Inject
155    public MemcachedWrapper(CacheModule cacheModule, CacheMonitor cacheMonitor, MemcachedClientIF client, Integer blockingTimeout, String name) {
156  18 super(cacheModule);
157  18 this.client = client;
158  18 this.cacheMonitor = cacheMonitor;
159  18 this.blockingTimeout = blockingTimeout;
160  18 this.name = name;
161    }
162   
 
163  49 toggle private ValidKey getValidKey(Object key) {
164  49 if (!validKeys.containsKey(key)) {
165  11 validKeys.put(key, new ValidKey(key));
166    }
167  49 return validKeys.get(key);
168    }
169   
 
170  29 toggle @Override
171    public Object get(Object key) {
172  29 ReadWriteLockSync lock = getLockForKey(key);
173  29 acquiredLockForKey(key, lock, LockType.READ);
174  28 Object element;
175  28 try {
176  28 element = this.getQuiet(key);
177    } finally {
178  28 lock.unlock(LockType.READ);
179    }
180  28 if (element == null) {
181  7 acquiredLockForKey(key, lock, LockType.WRITE);
182  7 element = this.getQuiet(key);
183  7 if (element != null) {
184  0 lock.unlock(LockType.WRITE);
185    }
186    }
187  28 if (element instanceof CacheEntriesWithKeys) {
188  2 element = ((CacheEntriesWithKeys) element).get(key);
189  2 if (element == null) { //this may return null if there are more keys with same hashes, we cache only the first one of them
190  1 log.error("A valid item not found in cache. This happens when more cache keys have same hash. We keep in cache the first one. " +
191    "Provide a custom hash generator if this happens too often. See info.magnolia.module.cache.memcached.MemcachedWrapper#createHash");
192  1 return new EmptyUncacheableEntry(); //return uncacheable entry, otherwise we'd try to store on every request which would block other request for the same key
193    }
194    }
195  27 return element;
196    }
197   
 
198  35 toggle @Override
199    public Object getQuiet(Object key) {
200  35 final Object value;
201  35 final ValidKey validKey = this.getValidKey(key);
202  35 try {
203  35 value = client.get(validKey.getKey());
204    } catch (OperationTimeoutException e) {
205  0 log.error("Detected 1 thread stuck in generating response for {}. This might be temporary if obtaining the response is resource intensive or when accessing remote resources.", MgnlContext.getWebContext().getRequest().getRequestURL());
206  0 return null;
207    } catch (RuntimeException e) {
208  0 log.error("Can't get item from cache for : {}", MgnlContext.getWebContext().getRequest().getRequestURL());
209  0 return null;
210    }
211  35 super.get(key);
212  35 return value;
213    }
214   
 
215  0 toggle @Override
216    public boolean hasElement(Object key) {
217  0 return this.get(key) != null;
218    }
219   
 
220  14 toggle @Override
221    public void put(Object key, Object value) {
222  14 this.put(key, value, -1);
223    }
224   
 
225  14 toggle @Override
226    public void put(Object key, Object value, int timeToLiveInSeconds) {
227  14 ReadWriteLockSync lock = getLockForKey(key);
228  14 if (!lock.isHeldByCurrentThread(LockType.WRITE)) {
229  8 lock.lock(LockType.WRITE);
230    }
231  14 try {
232  14 if (value != null) {
233  14 ValidKey validKey = this.getValidKey(key);
234  14 if (validKey.isHash()) {
235  4 CacheEntriesWithKeys entryWithKey = new CacheEntriesWithKeys();
236  4 entryWithKey.put(key, value);
237  4 client.add(validKey.getKey(), timeToLiveInSeconds, entryWithKey);
238    } else {
239  10 client.add(validKey.getKey(), timeToLiveInSeconds, value);
240    }
241    }
242    } finally {
243    //Release the writelock here. This will have been acquired in the get, where the element was null
244  14 lock.unlock(LockType.WRITE);
245    }
246  14 super.put(key, value, timeToLiveInSeconds);
247    }
248   
 
249  0 toggle @Override
250    public void remove(Object key) {
251  0 client.delete(this.getValidKey(key).getKey());
252  0 validKeys.remove(key);
253  0 super.remove(key);
254    }
255   
 
256  2 toggle @Override
257    public void clear() {
258  2 try {
259  2 client.flush();
260  2 cacheMonitor.countFlush(this.name);
261  2 validKeys.clear();
262  2 super.clear();
263    } catch (IllegalStateException e) {
264  0 if ("Shutting down".equals(e.getMessage())) {
265  0 log.warn("Cannot flush cache '{}'. The memcached client has been shut down.", this.getName());
266    } else {
267  0 throw e;
268    }
269    }
270    }
271   
 
272  1 toggle @Override
273    public void unlock(Object key) {
274  1 ReadWriteLockSync lock = getLockForKey(key);
275  1 if (lock.isHeldByCurrentThread(LockType.WRITE)) {
276  1 lock.unlock(LockType.WRITE);
277    }
278    }
279   
 
280    toggle @Override
281    public int getBlockingTimeout() {
282    return blockingTimeout;
283    }
284   
 
285    toggle @Override
286    public String getName() {
287    return name;
288    }
289   
 
290    toggle @Override
291    public int getSize() {
292    return this.validKeys.size();
293    }
294   
 
295    toggle @Override
296    public Collection<Object> getKeys() {
297    return this.validKeys.keySet();
298    }
299   
 
300    toggle public Map<SocketAddress, Map<String, String>> getStats() {
301    return client.getStats();
302    }
303   
 
304  13 toggle protected void shutdown() {
305  13 client.shutdown();
306    }
307   
 
308  44 toggle private ReadWriteLockSync getLockForKey(final Object key) {
309  44 return cacheLockProvider.getSyncForKey(key);
310    }
311   
 
312  36 toggle private void acquiredLockForKey(final Object key, final ReadWriteLockSync lock, final LockType lockType) {
313  36 if (blockingTimeout > 0) {
314  34 try {
315  34 boolean acquired = lock.tryLock(lockType, blockingTimeout);
316  34 if (!acquired) {
317  1 StringBuilder message = new StringBuilder("Lock timeout. Waited more than ")
318    .append(blockingTimeout)
319    .append("ms to acquire lock for key ")
320    .append(key).append(" on blocking cache ").append(this.getName());
321  1 throw new MgnlLockTimeoutException(message.toString());
322    }
323    } catch (InterruptedException e) {
324  0 throw new MgnlLockTimeoutException("Got interrupted while trying to acquire lock for key " + key, e);
325    }
326    } else {
327  2 lock.lock(lockType);
328    }
329    }
330   
331    }