View Javadoc
1   /**
2    * This file Copyright (c) 2008-2018 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.module.cache.cachepolicy;
35  
36  import info.magnolia.cms.cache.CacheConstants;
37  import info.magnolia.cms.core.AggregationState;
38  import info.magnolia.context.MgnlContext;
39  import info.magnolia.context.WebContext;
40  import info.magnolia.module.cache.Cache;
41  import info.magnolia.module.cache.CacheModule;
42  import info.magnolia.module.cache.CachePolicy;
43  import info.magnolia.module.cache.CachePolicyResult;
44  import info.magnolia.module.cache.FlushPolicy;
45  import info.magnolia.module.cache.cachekey.CacheKeyGenerator;
46  import info.magnolia.module.cache.cachekey.DefaultCacheKey;
47  import info.magnolia.module.cache.cachekey.DefaultCacheKeyGenerator;
48  import info.magnolia.module.cache.cachepolicy.instructor.CacheInstructor;
49  import info.magnolia.module.cache.cachepolicy.voters.BrowserTtlVoter;
50  import info.magnolia.module.cache.cachepolicy.voters.ServerTtlVoter;
51  import info.magnolia.module.cache.cachepolicy.voters.TtlVoting;
52  import info.magnolia.module.cache.filter.CacheResponseWrapper;
53  import info.magnolia.module.cache.filter.UncacheableEntry;
54  import info.magnolia.voting.voters.VoterSet;
55  
56  import java.util.Collections;
57  import java.util.HashSet;
58  import java.util.Set;
59  import java.util.function.BiConsumer;
60  
61  import javax.inject.Inject;
62  import javax.inject.Provider;
63  
64  import org.slf4j.Logger;
65  import org.slf4j.LoggerFactory;
66  
67  /**
68   * A basic CachePolicy driven by shouldBypassVoters. This policy implementation uses {@link info.magnolia.module.cache.cachekey.DefaultCacheKey} to identify each cache entry.
69   */
70  public class Default implements CachePolicy {
71  
72      public static final String UUID_KEY_MAP_KEY = "uuid-key-mapping";
73      private static final Logger log = LoggerFactory.getLogger(Default.class);
74  
75      private CacheKeyGenerator<?> cacheKeyGenerator = new DefaultCacheKeyGenerator();
76      private VoterSet shouldBypassVoters;
77      private VoterSet<CacheResponseWrapper> ttlVoters = new VoterSet<>();
78  
79      private boolean refreshOnNoCacheRequests = false;
80  
81      private final CacheModule cacheModule;
82      private final Provider<CacheInstructor> cacheInstructorProvider;
83  
84      @Inject
85      public Default(CacheModule cacheModule, Provider<CacheInstructor> cacheInstructorProvider) {
86          this.cacheModule = cacheModule;
87          this.cacheInstructorProvider = cacheInstructorProvider;
88          ttlVoters.setVoting(new TtlVoting());
89          ttlVoters.addVoter(new ServerTtlVoter(cacheInstructorProvider));
90          ttlVoters.addVoter(new BrowserTtlVoter());
91      }
92  
93  
94      @Override
95      public CachePolicyResult shouldCache(final Cache cache, final AggregationState aggregationState, final FlushPolicy flushPolicy) {
96          final Object key = retrieveCacheKey(aggregationState);
97  
98          if (shouldBypass(aggregationState, key)) {
99              return new CachePolicyResult(CachePolicyResult.bypass, key, null);
100         }
101 
102         if (shouldRefresh(aggregationState, key)) {
103             log.debug("Cache refresh requested for {}", key);
104             return new CachePolicyResult(CachePolicyResult.store, key, null);
105         }
106 
107         // no multithreaded expiring cache can guarantee existence of the key between two subsequent calls without synchronization here on common object or mutex
108         // and synchronization on the cache is too heavy weight
109         // if (cache.hasElement(key)) {
110         // final Object entry = cache.get(key);
111         // ... so unless having mutexes for our own cache keys and being able to synchronize on that
112         // simply make sure there is just one call ... ever. Either underlying cache has it's own key-mutex system (like ehCache) or
113         // stale value returned by cache might result in entries being generated multiple times or served stale for while ... so choose your underlying cache impl carefully
114         // either way, Magnolia should not need to care nor should it play a police for cache implementation
115 
116         // for default cache impl (blocking ehCache) this call will block on given key, and on given key only, if previously requested until value is available or timeout occurs
117         final Object cachedEntry = cache.get(key);
118         // also assume that if the value can't be retrieved for given key, underlying cache is not a pig and will throw exception at some point releasing the thread and propagating the error to the user
119 
120         if (cachedEntry != null) {
121             if (cachedEntry instanceof UncacheableEntry) {
122                 return new CachePolicyResult(CachePolicyResult.bypass, key, null);
123             } else {
124                 return new CachePolicyResult(CachePolicyResult.useCache, key, cachedEntry);
125             }
126         }
127         return new CachePolicyResult(CachePolicyResult.store, key, null);
128     }
129 
130     /**
131      * Checks whether requested content should be served from cache or refreshed instead.
132      *
133      * @return True if cache entry for the key should be recreated, false otherwise.
134      */
135     protected boolean shouldRefresh(AggregationState aggregationState, Object key) {
136         if (isRefreshOnNoCacheRequests()) {
137             String cacheControl = ((WebContext) MgnlContext.getInstance()).getRequest().getHeader(CacheConstants.HEADER_CACHE_CONTROL);
138             String pragma = ((WebContext) MgnlContext.getInstance()).getRequest().getHeader(CacheConstants.HEADER_PRAGMA);
139             return (cacheControl != null && cacheControl.equals(CacheConstants.HEADER_VALUE_NO_CACHE)) || (pragma != null && pragma.equals(CacheConstants.HEADER_VALUE_NO_CACHE));
140         }
141         return false;
142     }
143 
144     protected boolean shouldBypass(AggregationState aggregationState, Object key) {
145         final String uri;
146         if (key instanceof DefaultCacheKey) {
147             uri = ((DefaultCacheKey) key).getUri();
148         } else {
149             uri = key.toString();
150         }
151         // true if shouldBypassVoters vote positively
152         if (shouldBypassVoters != null) {
153             return shouldBypassVoters.vote(uri) <= 0;
154         }
155         log.warn("No cache voter defined.");
156         return false;
157     }
158 
159     public Object retrieveCacheKey(final AggregationState aggregationState) {
160         CacheKeyGenerator<?> cacheKeyGenerator = cacheInstructorProvider.get().getKeyGenerator();
161         if (cacheKeyGenerator == null) {
162             cacheKeyGenerator = getCacheKeyGenerator();
163         }
164         return cacheKeyGenerator.generateKey(aggregationState);
165     }
166 
167     @Override
168     public Object[] retrieveCacheKeys(final String uuid, final String repository) {
169         final String uuidKey = repository + ":" + uuid;
170         final Set<Object> keys = getUUIDKeySetFromCacheSafely(uuidKey);
171         return keys.toArray();
172     }
173 
174     @Override
175     public void persistCacheKey(final String repo, final String uuid, final Object key) {
176         final String uuidKey = repo + ":" + uuid;
177         final Cache cache = getUuidKeyCache();
178         // need to be thread safe to prevent losing keys added by other threads while manipulating collection
179         threadSafeUUIDCacheOp(cache, uuidKey, (c, k) -> {
180             k.add(key);
181             c.put(uuidKey, k);
182         });
183     }
184 
185     private Set<Object> threadSafeUUIDCacheOp(final Cache cache, final String uuidKey, final BiConsumer<Cache, Set<Object>> c) {
186         synchronized (cache) {
187             Set<Object> keys = (Set<Object>) cache.get(uuidKey);
188             if (keys == null) {
189                 keys = Collections.synchronizedSet(new HashSet<>());
190             }
191             c.accept(cache, keys);
192             return keys;
193         }
194     }
195 
196     @Override
197     public Object[] removeCacheKeys(final String uuid, final String repository) {
198         final String uuidKey = repository + ":" + uuid;
199         final Cache cache = getUuidKeyCache();
200         // need to be thread safe to prevent losing keys added by other threads while manipulating collection
201         return threadSafeUUIDCacheOp(cache, uuidKey, (c, k) -> c.remove(uuidKey)).toArray();
202     }
203 
204     private Cache getUuidKeyCache() {
205         return cacheModule.getCacheFactory().getCache(UUID_KEY_MAP_KEY);
206     }
207 
208     /**
209      * Method to safely (without danger of blocking cache) obtain persistent mapping between UUIDs and cache keys.
210      * Obtained mapping is a __COPY__ of actual mapping in cache. Any changes to it are __NOT__ persisted in cache itself!!!
211      */
212     private synchronized Set<Object> getUUIDKeySetFromCacheSafely(String uuidKey) {
213         final Cache cache = getUuidKeyCache();
214         synchronized (cache) {
215             // need to be thread safe to prevent loosing keys added by other threads while manipulating collection
216             return threadSafeUUIDCacheOp(cache, uuidKey, (c, k) -> c.put(uuidKey, k));
217         }
218     }
219 
220     public boolean isRefreshOnNoCacheRequests() {
221         return this.refreshOnNoCacheRequests;
222     }
223 
224     public void setRefreshOnNoCacheRequests(boolean allowNoCacheHeader) {
225         this.refreshOnNoCacheRequests = allowNoCacheHeader;
226     }
227 
228     @Override
229     public VoterSet<CacheResponseWrapper> getTtlVoters() {
230         return ttlVoters;
231     }
232 
233     public void setTtlVoters(VoterSet<CacheResponseWrapper> ttlVoters) {
234         this.ttlVoters = ttlVoters;
235     }
236 
237     public VoterSet getShouldBypassVoters() {
238         return shouldBypassVoters;
239     }
240 
241     public void setShouldBypassVoters(VoterSet shouldBypassVoters) {
242         this.shouldBypassVoters = shouldBypassVoters;
243     }
244 
245     public CacheKeyGenerator<?> getCacheKeyGenerator() {
246         return cacheKeyGenerator;
247     }
248 
249     public void setCacheKeyGenerator(CacheKeyGenerator<?> cacheKeyGenerator) {
250         this.cacheKeyGenerator = cacheKeyGenerator;
251     }
252 }