Skip to contentMethod: releaseWriteLock()
1: /*
2: * JOPA
3: * Copyright (C) 2023 Czech Technical University in Prague
4: *
5: * This library is free software; you can redistribute it and/or
6: * modify it under the terms of the GNU Lesser General Public
7: * License as published by the Free Software Foundation; either
8: * version 3.0 of the License, or (at your option) any later version.
9: *
10: * This library is distributed in the hope that it will be useful,
11: * but WITHOUT ANY WARRANTY; without even the implied warranty of
12: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13: * Lesser General Public License for more details.
14: *
15: * You should have received a copy of the GNU Lesser General Public
16: * License along with this library.
17: */
18: package cz.cvut.kbss.jopa.sessions.cache;
19:
20: import cz.cvut.kbss.jopa.model.JOPAPersistenceProperties;
21: import cz.cvut.kbss.jopa.model.descriptors.Descriptor;
22: import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor;
23: import org.slf4j.Logger;
24: import org.slf4j.LoggerFactory;
25:
26: import java.net.URI;
27: import java.util.ArrayList;
28: import java.util.HashMap;
29: import java.util.HashSet;
30: import java.util.List;
31: import java.util.Map;
32: import java.util.Map.Entry;
33: import java.util.Objects;
34: import java.util.Set;
35: import java.util.concurrent.Executors;
36: import java.util.concurrent.Future;
37: import java.util.concurrent.ScheduledExecutorService;
38: import java.util.concurrent.TimeUnit;
39: import java.util.concurrent.locks.Lock;
40: import java.util.concurrent.locks.ReentrantReadWriteLock;
41:
42: /**
43: * Manages the second level cache shared by all persistence contexts.
44: * <p>
45: * This implementation of CacheManager uses cache-wide locking, i. e. the whole cache is locked when an entity is being
46: * put in it, no matter that only one context is affected by the change.
47: * <p>
48: * This cache is swept regularly by a dedicated thread, which removes all entries whose time-to-live (TTL) has
49: * expired.
50: */
51: public class TtlCacheManager implements CacheManager {
52:
53: private static final Logger LOG = LoggerFactory.getLogger(TtlCacheManager.class);
54:
55: /**
56: * Default time to live in millis
57: */
58: private static final long DEFAULT_TTL = 60000L;
59: // Initial delay is the sweep rate multiplied by this multiplier
60: private static final int DELAY_MULTIPLIER = 2;
61: /**
62: * Default sweep rate in millis
63: */
64: private static final long DEFAULT_SWEEP_RATE = 30000L;
65:
66: private Set<Class<?>> inferredClasses;
67:
68: private TtlCache cache;
69:
70: // Each repository can have its own lock and they could be acquired by this
71: // instance itself, no need to pass this burden to callers
72: private final Lock readLock;
73: private final Lock writeLock;
74:
75: private final ScheduledExecutorService sweeperScheduler;
76: private Future<?> sweeperFuture;
77: private long initDelay;
78: private long sweepRate;
79: private long timeToLive;
80: private volatile boolean sweepRunning;
81:
82: public TtlCacheManager(Map<String, String> properties) {
83: this.cache = new TtlCache();
84: initSettings(properties);
85: final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
86: this.readLock = lock.readLock();
87: this.writeLock = lock.writeLock();
88: this.sweeperScheduler = Executors.newSingleThreadScheduledExecutor();
89: this.sweeperFuture = sweeperScheduler.scheduleAtFixedRate(new CacheSweeper(), initDelay, sweepRate,
90: TimeUnit.MILLISECONDS);
91: }
92:
93: private void initSettings(Map<String, String> properties) {
94: if (!properties.containsKey(JOPAPersistenceProperties.CACHE_TTL)) {
95: this.timeToLive = DEFAULT_TTL;
96: } else {
97: final String strCacheTtl = properties.get(JOPAPersistenceProperties.CACHE_TTL);
98: try {
99: // The property is in seconds, we need milliseconds
100: this.timeToLive = Long.parseLong(strCacheTtl) * 1000;
101: } catch (NumberFormatException e) {
102: LOG.warn("Unable to parse cache time to live setting value {}, using default value.", strCacheTtl);
103: this.timeToLive = DEFAULT_TTL;
104: }
105: }
106: if (!properties.containsKey(JOPAPersistenceProperties.CACHE_SWEEP_RATE)) {
107: this.sweepRate = DEFAULT_SWEEP_RATE;
108: } else {
109: final String strSweepRate = properties
110: .get(JOPAPersistenceProperties.CACHE_SWEEP_RATE);
111: try {
112: // The property is in seconds, we need milliseconds
113: this.sweepRate = Long.parseLong(strSweepRate) * 1000;
114: } catch (NumberFormatException e) {
115: LOG.warn("Unable to parse sweep rate setting value {}, using default value.", strSweepRate);
116: this.sweepRate = DEFAULT_SWEEP_RATE;
117: }
118: }
119: this.initDelay = DELAY_MULTIPLIER * sweepRate;
120: }
121:
122: @Override
123: public void close() {
124: if (sweeperFuture != null) {
125: LOG.debug("Stopping cache sweeper.");
126: sweeperFuture.cancel(true);
127: sweeperScheduler.shutdown();
128: }
129: this.sweeperFuture = null;
130: evictAll();
131: }
132:
133: @Override
134: public void add(Object identifier, Object entity, Descriptors descriptors) {
135: Objects.requireNonNull(identifier);
136: Objects.requireNonNull(entity);
137: Objects.requireNonNull(descriptors);
138:
139: acquireWriteLock();
140: try {
141: cache.put(identifier, entity, descriptors);
142: } finally {
143: releaseWriteLock();
144: }
145: }
146:
147: @Override
148: public LoadStateDescriptor<?> getLoadStateDescriptor(Object instance) {
149: if (instance == null) {
150: return null;
151: }
152: readLock.lock();
153: try {
154: return cache.getLoadStateDescriptor(instance);
155: } finally {
156: readLock.unlock();
157: }
158: }
159:
160: /**
161: * Releases the live object cache.
162: */
163: private void releaseCache() {
164: acquireWriteLock();
165: try {
166: this.cache = new TtlCache();
167: } finally {
168: releaseWriteLock();
169: }
170: }
171:
172: @Override
173: public void evictInferredObjects() {
174: acquireWriteLock();
175: try {
176: getInferredClasses().forEach(cache::evict);
177: } finally {
178: releaseWriteLock();
179: }
180: }
181:
182: @Override
183: public <T> T get(Class<T> cls, Object identifier, Descriptor descriptor) {
184: if (cls == null || identifier == null || descriptor == null) {
185: return null;
186: }
187: acquireReadLock();
188: try {
189: return cache.get(cls, identifier, descriptor);
190: } finally {
191: releaseReadLock();
192: }
193: }
194:
195: /**
196: * Get the set of inferred classes.
197: * <p>
198: * Inferred classes (i.e. classes with inferred attributes) are tracked separately since they require special
199: * behavior.
200: *
201: * @return Set of inferred classes
202: */
203: public Set<Class<?>> getInferredClasses() {
204: if (inferredClasses == null) {
205: this.inferredClasses = new HashSet<>();
206: }
207: return inferredClasses;
208: }
209:
210: /**
211: * Set the inferred classes.
212: * <p>
213: * For more information about inferred classes see {@link #getInferredClasses()}.
214: *
215: * @param inferredClasses The set of inferred classes
216: */
217: @Override
218: public void setInferredClasses(Set<Class<?>> inferredClasses) {
219: this.inferredClasses = inferredClasses;
220: }
221:
222: @Override
223: public boolean contains(Class<?> cls, Object identifier, Descriptor descriptor) {
224: if (cls == null || identifier == null || descriptor == null) {
225: return false;
226: }
227: acquireReadLock();
228: try {
229: return cache.contains(cls, identifier, descriptor);
230: } finally {
231: releaseReadLock();
232: }
233: }
234:
235: @Override
236: public void evict(Class<?> cls) {
237: Objects.requireNonNull(cls);
238:
239: acquireWriteLock();
240: try {
241: cache.evict(cls);
242: } finally {
243: releaseWriteLock();
244: }
245: }
246:
247: @Override
248: public void evict(Class<?> cls, Object identifier, URI context) {
249: Objects.requireNonNull(cls);
250: Objects.requireNonNull(identifier);
251:
252: acquireWriteLock();
253: try {
254: cache.evict(cls, identifier, context);
255: } finally {
256: releaseWriteLock();
257: }
258:
259: }
260:
261: @Override
262: public void evict(URI context) {
263: acquireWriteLock();
264: try {
265: cache.evict(context);
266: } finally {
267: releaseWriteLock();
268: }
269: }
270:
271: @Override
272: public void evictAll() {
273: releaseCache();
274: }
275:
276: private void acquireReadLock() {
277: readLock.lock();
278: }
279:
280: private void releaseReadLock() {
281: readLock.unlock();
282: }
283:
284: private void acquireWriteLock() {
285: writeLock.lock();
286: }
287:
288: private void releaseWriteLock() {
289: writeLock.unlock();
290: }
291:
292: /**
293: * Sweeps the second level cache and removes entities with no more time to live.
294: */
295: private final class CacheSweeper implements Runnable {
296:
297: @Override
298: public void run() {
299: LOG.trace("Running cache sweep.");
300: TtlCacheManager.this.acquireWriteLock();
301: try {
302: if (TtlCacheManager.this.sweepRunning) {
303: return;
304: }
305: TtlCacheManager.this.sweepRunning = true;
306: final long currentTime = System.currentTimeMillis();
307: final List<URI> toEvict = new ArrayList<>();
308: // Mark the objects for eviction (can't evict them now, it would
309: // cause ConcurrentModificationException)
310: for (Entry<URI, Long> e : cache.ttl.entrySet()) {
311: final long lm = e.getValue();
312: if (lm + TtlCacheManager.this.timeToLive < currentTime) {
313: toEvict.add(e.getKey());
314: }
315: }
316: // Evict them
317: toEvict.forEach(TtlCacheManager.this::evict);
318: } finally {
319: TtlCacheManager.this.sweepRunning = false;
320: TtlCacheManager.this.releaseWriteLock();
321: }
322: }
323: }
324:
325: private static final class TtlCache extends EntityCache {
326:
327: private final Map<URI, Long> ttl = new HashMap<>();
328:
329: @Override
330: void put(Object identifier, Object entity, Descriptors descriptors) {
331: if (!isCacheable(descriptors.repositoryDescriptor())) {
332: return;
333: }
334: super.put(identifier, entity, descriptors);
335: final URI ctx = descriptors.repositoryDescriptor().getSingleContext().orElse(defaultContext);
336: updateTimeToLive(ctx);
337: }
338:
339: @Override
340: <T> T get(Class<T> cls, Object identifier, Descriptor descriptor) {
341: assert cls != null;
342: assert identifier != null;
343:
344: return getInternal(cls, identifier, descriptor, this::updateTimeToLive);
345: }
346:
347: private void updateTimeToLive(URI context) {
348: assert context != null;
349:
350: ttl.put(context, System.currentTimeMillis());
351: }
352:
353: @Override
354: void evict(URI context) {
355: final URI ctx = context != null ? context : defaultContext;
356: super.evict(ctx);
357: ttl.remove(context);
358: }
359:
360: @Override
361: void evict(Class<?> cls) {
362: for (Entry<URI, Map<Object, Map<Class<?>, Object>>> e : repoCache.entrySet()) {
363: final Map<Object, Map<Class<?>, Object>> m = e.getValue();
364: for (Entry<Object, Map<Class<?>, Object>> indNode : m.entrySet()) {
365: final Object instance = indNode.getValue().remove(cls);
366: if (instance != null) {
367: descriptors.remove(instance);
368: }
369: }
370: if (m.isEmpty()) {
371: ttl.remove(e.getKey());
372: }
373: }
374: }
375: }
376: }