/* * Copyright 2016 ThoughtWorks, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.thoughtworks.go.server.cache; import com.thoughtworks.go.domain.NullUser; import com.thoughtworks.go.domain.PersistentObject; import com.thoughtworks.go.server.transaction.TransactionSynchronizationManager; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import net.sf.ehcache.config.CacheConfiguration; import net.sf.ehcache.statistics.LiveCacheStatistics; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.springframework.cache.ehcache.EhCacheFactoryBean; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import static com.thoughtworks.go.util.ExceptionUtils.bomb; /** * @understands storing and retrieving objects from an underlying LRU cache */ public class GoCache { private final ThreadLocal<Boolean> doNotServeForTransaction = new ThreadLocal<>(); static final String SUB_KEY_DELIMITER = "!_#$#_!"; private Cache ehCache; private static final Logger LOGGER = Logger.getLogger(GoCache.class); private TransactionSynchronizationManager transactionSynchronizationManager; private final Set<Class<? extends PersistentObject>> nullObjectClasses; static class KeyList extends ArrayList<String> { } public GoCache(EhCacheFactoryBean ehCacheFactoryBean, TransactionSynchronizationManager transactionSynchronizationManager) { this((Cache) ehCacheFactoryBean.getObject(), transactionSynchronizationManager); } /** * @deprecated only for tests */ public GoCache(GoCache goCache) { this(goCache.ehCache, goCache.transactionSynchronizationManager); } private GoCache(Cache cache, TransactionSynchronizationManager transactionSynchronizationManager) { this.ehCache = cache; this.transactionSynchronizationManager = transactionSynchronizationManager; this.nullObjectClasses = new HashSet<>(); nullObjectClasses.add(NullUser.class); registerAsCacheEvictionListener(); } protected void registerAsCacheEvictionListener() { ehCache.getCacheEventNotificationService().registerListener(new CacheEvictionListener(this)); } public void stopServingForTransaction() { if (transactionSynchronizationManager.isTransactionBodyExecuting() && !doNotServeForTransaction()) { doNotServeForTransaction.set(true); transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void beforeCompletion() { doNotServeForTransaction.set(false); } }); } } public void put(String key, Object value) { put(key, value, new TransactionActivityPredicate()); } private void put(String key, Object value, Predicate predicate) { logUnsavedPersistentObjectInteraction(value, "PersistentObject %s added to cache without an id."); if (predicate.isTrue()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("transaction active during cache put for %s = %s", key, value), new IllegalStateException()); } return; } ehCache.put(new Element(key, value)); } public List<String> getKeys() { return ehCache.getKeys(); } /** * SHOULD ONLY BE USED IN AN AFTER-COMMIT CALLBACK. In all other cases you should be using put() which ensures that * no transaction is active at the moment before putting the value. This ensures that you don't end up having data * in cache which is invalid because the transaction has rolled back. * * @param key * @param value */ public void putInAfterCommit(String key, Object value) { put(key, value, new InTransactionBodyPredicate()); } private void logUnsavedPersistentObjectInteraction(Object value, String message) { if (value instanceof PersistentObject) { for (Class<? extends PersistentObject> nullObjectClass : nullObjectClasses) { if (value.getClass().equals(nullObjectClass)) { return; } } PersistentObject persistentObject = (PersistentObject) value; if (!persistentObject.hasId()) { String msg = String.format(message, persistentObject); IllegalStateException exception = new IllegalStateException(); LOGGER.error(msg, exception); throw bomb(msg, exception); } } } public void flush() { ehCache.flush(); } public Object get(String key) { if (doNotServeForTransaction()) { return null; } return getWithoutTransactionCheck(key); } private Object getWithoutTransactionCheck(String key) { Element element = ehCache.get(key); if (element == null) { return null; } Object value = element.getObjectValue(); logUnsavedPersistentObjectInteraction(value, "PersistentObject %s without an id served out of cache."); return value; } private boolean doNotServeForTransaction() { return doNotServeForTransaction.get() != null && doNotServeForTransaction.get(); } public void clear() { ehCache.removeAll(); } public boolean remove(String key) { synchronized (key.intern()) { Object value = getWithoutTransactionCheck(key); if (value instanceof KeyList) { for (String subKey : (KeyList) value) { ehCache.remove(compositeKey(key, subKey)); } } return ehCache.remove(key); } } public Object get(String key, String subKey) { return get(compositeKey(key, subKey)); } public void put(String key, String subKey, Object value) { KeyList subKeys; synchronized (key.intern()) { subKeys = subKeyFamily(key); if (subKeys == null) { subKeys = new KeyList(); put(key, subKeys); } subKeys.add(subKey); } put(compositeKey(key, subKey), value); } public void removeAll(List<String> keys) { for (String key : keys) { remove(key); } } public void removeAssociations(String key, Element element) { if (element.getObjectValue() instanceof KeyList) { synchronized (key.intern()) { for (String subkey : (KeyList) element.getObjectValue()) { remove(compositeKey(key, subkey)); } } } else if (key.contains(SUB_KEY_DELIMITER)) { String[] parts = StringUtils.splitByWholeSeparator(key, SUB_KEY_DELIMITER); String parentKey = parts[0]; String childKey = parts[1]; synchronized (parentKey.intern()) { Element parent = ehCache.get(parentKey); if (parent == null) { return; } GoCache.KeyList subKeys = (GoCache.KeyList) parent.getObjectValue(); subKeys.remove(childKey); } } } public boolean isKeyInCache(Object key) { return ehCache.isKeyInCache(key); } private KeyList subKeyFamily(String parentKey) { return (KeyList) get(parentKey); } private String compositeKey(String key, String subKey) { String concat = key + subKey; if (concat.contains(SUB_KEY_DELIMITER)) { bomb(String.format("Base and sub key concatenation(key = %s, subkey = %s) must not have pattern %s", key, subKey, SUB_KEY_DELIMITER)); } return key + SUB_KEY_DELIMITER + subKey; } public void remove(String key, String subKey) { synchronized (key.intern()) { KeyList subKeys = subKeyFamily(key); subKeys.remove(subKey); remove(compositeKey(key, subKey)); } } public LiveCacheStatistics statistics() { return ehCache.getLiveCacheStatistics(); } public CacheConfiguration configuration() { return ehCache.getCacheConfiguration(); } private interface Predicate { boolean isTrue(); } private class TransactionActivityPredicate implements Predicate { public boolean isTrue() { return transactionSynchronizationManager.isActualTransactionActive(); } } private class InTransactionBodyPredicate implements Predicate { public boolean isTrue() { return transactionSynchronizationManager.isTransactionBodyExecuting(); } } }