/*
* Copyright (C) 2005-2010 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see
* It uses the Ehcache Cache for it's per-transaction * caches as these provide automatic size limitations, etc. *
* Instances of this class do not require a transaction. They will work * directly with the shared cache when no transaction is present. There is * virtually no overhead when running out-of-transaction. *
* The first phase of the commit ensures that any values written to the cache in the * current transaction are not already superceded by values in the shared cache. In * this case, the transaction is failed for concurrency reasons and will have to retry. * The second phase occurs post-commit. We are sure that the transaction committed * correctly, but things may have changed in the cache between the commit and post-commit. * If this is the case, then the offending values are merely removed from the shared * cache. *
* When the cache is {@link #clear() cleared}, a flag is set on the transaction. * The shared cache, instead of being cleared itself, is just ignored for the remainder * of the tranasaction. At the end of the transaction, if the flag is set, the * shared transaction is cleared before updates are added back to it. *
* Because there is a limited amount of space available to the in-transaction caches,
* when either of these becomes full, the cleared flag is set. This ensures that
* the shared cache will not have stale data in the event of the transaction-local
* caches dropping items. It is therefore important to size the transactional caches
* correctly.
*
* @author Derek Hulley
*/
public class TransactionalCache
* The removed list will overflow to disk in order to ensure that deletions are
* not lost.
*
* @param maxCacheSize
*/
public void setMaxCacheSize(int maxCacheSize)
{
this.maxCacheSize = maxCacheSize;
}
/**
* Set the name that identifies this cache from other instances. This is optional.
*
* @param name
*/
public void setName(String name)
{
this.name = name;
}
/**
* Ensures that all properties have been set
*/
public void afterPropertiesSet() throws Exception
{
Assert.notNull(name, "name property not set");
Assert.notNull(cacheManager, "cacheManager property not set");
// generate the resource binding key
resourceKeyTxnData = RESOURCE_KEY_TXN_DATA + "." + name;
// Refine the log category
logger = LogFactory.getLog(TransactionalCache.class.getName() + "." + name);
isDebugEnabled = logger.isDebugEnabled();
}
/**
* To be used in a transaction only.
*/
private TransactionData getTransactionData()
{
TransactionData data = (TransactionData) AlfrescoTransactionSupport.getResource(resourceKeyTxnData);
if (data == null)
{
String txnId = AlfrescoTransactionSupport.getTransactionId();
data = new TransactionData();
// create and initialize caches
data.updatedItemsCache = new Cache(
name + "_"+ txnId + "_updates",
maxCacheSize, false, true, 0, 0);
data.removedItemsCache = new Cache(
name + "_" + txnId + "_removes",
maxCacheSize, false, true, 0, 0);
try
{
cacheManager.addCache(data.updatedItemsCache);
cacheManager.addCache(data.removedItemsCache);
}
catch (CacheException e)
{
throw new AlfrescoRuntimeException("Failed to add txn caches to manager", e);
}
finally
{
// ensure that we get the transaction callbacks as we have bound the unique
// transactional caches to a common manager
AlfrescoTransactionSupport.bindListener(this);
}
AlfrescoTransactionSupport.bindResource(resourceKeyTxnData, data);
}
return data;
}
/**
* Checks the transactional removed and updated caches before checking the shared cache.
*/
public boolean contains(K key)
{
Object value = get(key);
if (value == null)
{
return false;
}
else
{
return true;
}
}
/**
* The keys returned are a union of the set of keys in the current transaction and
* those in the backing cache.
*/
@SuppressWarnings("unchecked")
public Collection
* Where a transaction is present, a cache of updated items is lazily added to the
* thread and the Object put onto that.
*/
public void put(K key, V value)
{
// are we in a transaction?
if (AlfrescoTransactionSupport.getTransactionId() == null) // not in transaction
{
// no transaction
sharedCache.put(key, value);
// done
if (isDebugEnabled)
{
logger.debug("No transaction - adding item direct to shared cache: \n" +
" cache: " + this + "\n" +
" key: " + key + "\n" +
" value: " + value);
}
}
else // transaction present
{
TransactionData txnData = getTransactionData();
// Ensure that the cache isn't being modified
if (txnData.isClosed)
{
if (isDebugEnabled)
{
logger.debug(
"In post-commit add: \n" +
" cache: " + this + "\n" +
" key: " + key + "\n" +
" value: " + value);
}
}
else
{
// we have an active transaction - add the item into the updated cache for this transaction
// are we in an overflow condition?
if (txnData.updatedItemsCache.getMemoryStoreSize() >= maxCacheSize)
{
// overflow about to occur or has occured - we can only guarantee non-stale
// data by clearing the shared cache after the transaction. Also, the
// shared cache needs to be ignored for the rest of the transaction.
txnData.isClearOn = true;
if (!txnData.haveIssuedFullWarning && logger.isWarnEnabled())
{
logger.warn("Transactional update cache '" + name + "' is full (" + maxCacheSize + ").");
txnData.haveIssuedFullWarning = true;
}
}
CacheBucket
* Where a transaction is present, a cache of removed items is lazily added to the
* thread and the Object put onto that.
*/
public void remove(K key)
{
// are we in a transaction?
if (AlfrescoTransactionSupport.getTransactionId() == null) // not in transaction
{
// no transaction
sharedCache.remove(key);
// done
if (isDebugEnabled)
{
logger.debug("No transaction - removing item from shared cache: \n" +
" cache: " + this + "\n" +
" key: " + key);
}
}
else // transaction present
{
TransactionData txnData = getTransactionData();
// Ensure that the cache isn't being modified
if (txnData.isClosed)
{
if (isDebugEnabled)
{
logger.debug(
"In post-commit remove: \n" +
" cache: " + this + "\n" +
" key: " + key);
}
}
else
{
// is the shared cache going to be cleared?
if (txnData.isClearOn)
{
// don't store removals if we're just going to clear it all out later
}
else
{
// are we in an overflow condition?
if (txnData.removedItemsCache.getMemoryStoreSize() >= maxCacheSize)
{
// overflow about to occur or has occured - we can only guarantee non-stale
// data by clearing the shared cache after the transaction. Also, the
// shared cache needs to be ignored for the rest of the transaction.
txnData.isClearOn = true;
if (!txnData.haveIssuedFullWarning && logger.isWarnEnabled())
{
logger.warn("Transactional removal cache '" + name + "' is full (" + maxCacheSize + ").");
txnData.haveIssuedFullWarning = true;
}
}
else
{
V existingValue = getSharedCacheValue(key);
if (existingValue == null)
{
// There is no point doing a remove for a value that doesn't exist
}
else
{
// Create a bucket to remove the value from the shared cache
CacheBucket
* The cache assumes the presence of a marker object to
*
* @author Derek Hulley
*/
private static class NewCacheBucket