1 |
|
|
2 |
|
|
3 |
|
|
4 |
|
|
5 |
|
|
6 |
|
|
7 |
|
|
8 |
|
|
9 |
|
|
10 |
|
|
11 |
|
|
12 |
|
|
13 |
|
|
14 |
|
|
15 |
|
|
16 |
|
|
17 |
|
|
18 |
|
|
19 |
|
|
20 |
|
|
21 |
|
|
22 |
|
|
23 |
|
|
24 |
|
|
25 |
|
|
26 |
|
|
27 |
|
|
28 |
|
|
29 |
|
|
30 |
|
|
31 |
|
|
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 |
|
|
72 |
|
|
|
|
| 82.3% |
Uncovered Elements: 20 (113) |
Complexity: 30 |
Complexity Density: 0.4 |
|
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 |
|
|
86 |
|
|
87 |
|
|
88 |
|
|
89 |
|
|
90 |
|
|
91 |
|
|
92 |
|
|
|
|
| - |
Uncovered Elements: 0 (0) |
Complexity: 0 |
Complexity Density: - |
|
93 |
|
protected static class CacheEntriesWithKeys extends HashMap<Object, Object> implements Serializable { |
94 |
|
} |
95 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (13) |
Complexity: 4 |
Complexity Density: 0.5 |
|
96 |
|
private class ValidKey { |
97 |
|
private final String key; |
98 |
|
private final boolean isHash; |
99 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (8) |
Complexity: 2 |
Complexity Density: 0.33 |
|
100 |
11 |
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 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (1) |
Complexity: 1 |
Complexity Density: 1 |
|
111 |
14 |
private boolean isHash() {... |
112 |
14 |
return isHash; |
113 |
|
} |
114 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (1) |
Complexity: 1 |
Complexity Density: 1 |
|
115 |
49 |
private String getKey() {... |
116 |
49 |
return key; |
117 |
|
} |
118 |
|
} |
119 |
|
|
120 |
|
|
121 |
|
|
122 |
|
|
|
|
| 50% |
Uncovered Elements: 2 (4) |
Complexity: 2 |
Complexity Density: 1 |
|
123 |
|
protected class EmptyUncacheableEntry extends UncacheableEntry { |
124 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (1) |
Complexity: 1 |
Complexity Density: 1 |
|
125 |
1 |
public EmptyUncacheableEntry() {... |
126 |
1 |
super(new CachedEntry() { |
|
|
| 0% |
Uncovered Elements: 1 (1) |
Complexity: 1 |
Complexity Density: 1 |
|
127 |
0 |
@Override... |
128 |
|
public void replay(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { |
129 |
0 |
throw new UnsupportedOperationException(); |
130 |
|
} |
131 |
|
|
|
|
| - |
Uncovered Elements: 0 (0) |
Complexity: 1 |
Complexity Density: - |
|
132 |
|
@Override... |
133 |
|
public String getOriginalURL() { |
134 |
|
return null; |
135 |
|
} |
136 |
|
|
|
|
| - |
Uncovered Elements: 0 (0) |
Complexity: 1 |
Complexity Density: - |
|
137 |
|
@Override... |
138 |
|
public long getLastModificationTime() { |
139 |
|
return 0; |
140 |
|
} |
141 |
|
|
|
|
| - |
Uncovered Elements: 0 (0) |
Complexity: 1 |
Complexity Density: - |
|
142 |
|
@Override... |
143 |
|
public int getTimeToLiveInSeconds() { |
144 |
|
throw new UnsupportedOperationException(); |
145 |
|
} |
146 |
|
}); |
147 |
|
} |
148 |
|
} |
149 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (1) |
Complexity: 1 |
Complexity Density: 1 |
|
150 |
1 |
protected String createHash(Object originalKey) {... |
151 |
1 |
return SecurityUtil.getMD5Hex(String.valueOf(originalKey)); |
152 |
|
} |
153 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (5) |
Complexity: 1 |
Complexity Density: 0.2 |
|
154 |
18 |
@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 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (5) |
Complexity: 2 |
Complexity Density: 0.67 |
|
163 |
49 |
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 |
|
|
|
|
| 92% |
Uncovered Elements: 2 (25) |
Complexity: 5 |
Complexity Density: 0.29 |
|
170 |
29 |
@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) { |
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(); |
193 |
|
} |
194 |
|
} |
195 |
27 |
return element; |
196 |
|
} |
197 |
|
|
|
|
| 60% |
Uncovered Elements: 4 (10) |
Complexity: 3 |
Complexity Density: 0.3 |
|
198 |
35 |
@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 |
|
|
|
|
| 0% |
Uncovered Elements: 1 (1) |
Complexity: 1 |
Complexity Density: 1 |
|
215 |
0 |
@Override... |
216 |
|
public boolean hasElement(Object key) { |
217 |
0 |
return this.get(key) != null; |
218 |
|
} |
219 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (1) |
Complexity: 1 |
Complexity Density: 1 |
|
220 |
14 |
@Override... |
221 |
|
public void put(Object key, Object value) { |
222 |
14 |
this.put(key, value, -1); |
223 |
|
} |
224 |
|
|
|
|
| 94.7% |
Uncovered Elements: 1 (19) |
Complexity: 4 |
Complexity Density: 0.31 |
|
225 |
14 |
@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 |
|
|
244 |
14 |
lock.unlock(LockType.WRITE); |
245 |
|
} |
246 |
14 |
super.put(key, value, timeToLiveInSeconds); |
247 |
|
} |
248 |
|
|
|
|
| 0% |
Uncovered Elements: 3 (3) |
Complexity: 1 |
Complexity Density: 0.33 |
|
249 |
0 |
@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 |
|
|
|
|
| 50% |
Uncovered Elements: 5 (10) |
Complexity: 3 |
Complexity Density: 0.38 |
|
256 |
2 |
@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 |
|
|
|
|
| 80% |
Uncovered Elements: 1 (5) |
Complexity: 2 |
Complexity Density: 0.67 |
|
272 |
1 |
@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 |
|
|
|
|
| - |
Uncovered Elements: 0 (0) |
Complexity: 1 |
Complexity Density: - |
|
280 |
|
@Override... |
281 |
|
public int getBlockingTimeout() { |
282 |
|
return blockingTimeout; |
283 |
|
} |
284 |
|
|
|
|
| - |
Uncovered Elements: 0 (0) |
Complexity: 1 |
Complexity Density: - |
|
285 |
|
@Override... |
286 |
|
public String getName() { |
287 |
|
return name; |
288 |
|
} |
289 |
|
|
|
|
| - |
Uncovered Elements: 0 (0) |
Complexity: 1 |
Complexity Density: - |
|
290 |
|
@Override... |
291 |
|
public int getSize() { |
292 |
|
return this.validKeys.size(); |
293 |
|
} |
294 |
|
|
|
|
| - |
Uncovered Elements: 0 (0) |
Complexity: 1 |
Complexity Density: - |
|
295 |
|
@Override... |
296 |
|
public Collection<Object> getKeys() { |
297 |
|
return this.validKeys.keySet(); |
298 |
|
} |
299 |
|
|
|
|
| - |
Uncovered Elements: 0 (0) |
Complexity: 1 |
Complexity Density: - |
|
300 |
|
public Map<SocketAddress, Map<String, String>> getStats() {... |
301 |
|
return client.getStats(); |
302 |
|
} |
303 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (1) |
Complexity: 1 |
Complexity Density: 1 |
|
304 |
13 |
protected void shutdown() {... |
305 |
13 |
client.shutdown(); |
306 |
|
} |
307 |
|
|
|
|
| 100% |
Uncovered Elements: 0 (1) |
Complexity: 1 |
Complexity Density: 1 |
|
308 |
44 |
private ReadWriteLockSync getLockForKey(final Object key) {... |
309 |
44 |
return cacheLockProvider.getSyncForKey(key); |
310 |
|
} |
311 |
|
|
|
|
| 91.7% |
Uncovered Elements: 1 (12) |
Complexity: 4 |
Complexity Density: 0.5 |
|
312 |
36 |
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 |
|
} |