Java tutorial
/* * Copyright 2019 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.Ehcache; import net.sf.ehcache.Element; import net.sf.ehcache.config.CacheConfiguration; import net.sf.ehcache.event.CacheEventListener; import net.sf.ehcache.statistics.StatisticsGateway; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.support.TransactionSynchronizationAdapter; 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<>(); public static final String SUB_KEY_DELIMITER = "!_#$#_!"; private Ehcache ehCache; private static final Logger LOGGER = LoggerFactory.getLogger(GoCache.class); private TransactionSynchronizationManager transactionSynchronizationManager; private final Set<Class<? extends PersistentObject>> nullObjectClasses; static class KeyList extends HashSet<String> { } /** * @deprecated only for tests */ public GoCache(GoCache goCache) { this(goCache.ehCache, goCache.transactionSynchronizationManager); } public GoCache(Ehcache cache, TransactionSynchronizationManager transactionSynchronizationManager) { this.ehCache = cache; this.transactionSynchronizationManager = transactionSynchronizationManager; this.nullObjectClasses = new HashSet<>(); nullObjectClasses.add(NullUser.class); registerAsCacheEvictionListener(); } public void removeListener(CacheEventListener cacheEventListener) { ehCache.getCacheEventNotificationService().unregisterListener(cacheEventListener); } public void addListener(CacheEventListener listener) { ehCache.getCacheEventNotificationService().registerListener(listener); } 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 {} added to cache without an id."); if (predicate.isTrue()) { LOGGER.debug("transaction active during cache put for {} = {}", 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 {} 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; } KeyList subKeys = (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); if (subKeys == null) { return; } subKeys.remove(subKey); remove(compositeKey(key, subKey)); } } public StatisticsGateway statistics() { return ehCache.getStatistics(); } 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(); } } }