/*
* (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Florent Guillaume
* Benoit Delbosc
*/
package org.nuxeo.ecm.core.storage.sql;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.management.MBeanServer;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.xa.XAException;
import javax.transaction.xa.Xid;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.management.ManagementService;
import net.sf.ehcache.transaction.manager.TransactionManagerLookup;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.storage.sql.ACLRow.ACLRowPositionComparator;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.management.ServerLocator;
import org.nuxeo.runtime.metrics.MetricsService;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import com.codahale.metrics.Timer.Context;
/**
* A {@link RowMapper} that use an unified ehcache.
* <p>
* The cache only holds {@link Row}s that are known to be identical to what's in the underlying {@link RowMapper}.
*/
public class UnifiedCachingRowMapper implements RowMapper {
private static final Log log = LogFactory.getLog(UnifiedCachingRowMapper.class);
private static final String ABSENT = "__ABSENT__\0\0\0";
private static CacheManager cacheManager = null;
protected static boolean isXA;
private Cache cache;
private Model model;
/**
* The {@link RowMapper} to which operations that cannot be processed from the cache are delegated.
*/
private RowMapper rowMapper;
/**
* The local invalidations due to writes through this mapper that should be propagated to other sessions at
* post-commit time.
*/
private final Invalidations localInvalidations;
/**
* The queue of invalidations received from other session or from the cluster invalidator, to process at
* pre-transaction time.
*/
private final InvalidationsQueue invalidationsQueue;
/**
* The propagator of invalidations to other mappers.
*/
private InvalidationsPropagator invalidationsPropagator;
private static final String CACHE_NAME = "unifiedVCSCache";
private static final String EHCACHE_FILE_PROP = "ehcacheFilePath";
private static AtomicInteger rowMapperCount = new AtomicInteger();
/**
* Cache statistics
*
* @since 5.7
*/
protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
protected Counter cacheHitCount;
protected Timer cacheGetTimer;
// sor means system of record (database access)
protected Counter sorRows;
protected Timer sorGetTimer;
public UnifiedCachingRowMapper() {
localInvalidations = new Invalidations();
invalidationsQueue = new InvalidationsQueue("mapper-" + this);
}
synchronized public void initialize(String repositoryName, Model model, RowMapper rowMapper,
InvalidationsPropagator invalidationsPropagator, Map<String, String> properties) {
this.model = model;
this.rowMapper = rowMapper;
this.invalidationsPropagator = invalidationsPropagator;
invalidationsPropagator.addQueue(invalidationsQueue);
if (cacheManager == null) {
if (properties.containsKey(EHCACHE_FILE_PROP)) {
String value = properties.get(EHCACHE_FILE_PROP);
log.info("Creating ehcache manager for VCS, using ehcache file: " + value);
cacheManager = CacheManager.create(value);
} else {
log.info("Creating ehcache manager for VCS, No ehcache file provided");
cacheManager = CacheManager.create();
}
isXA = cacheManager.getConfiguration().getCacheConfigurations().get(CACHE_NAME).isXaTransactional();
// Exposes cache to JMX
MBeanServer mBeanServer = Framework.getLocalService(ServerLocator.class).lookupServer();
ManagementService.registerMBeans(cacheManager, mBeanServer, true, true, true, true);
}
rowMapperCount.incrementAndGet();
cache = cacheManager.getCache(CACHE_NAME);
setMetrics(repositoryName);
}
protected void setMetrics(String repositoryName) {
cacheHitCount = registry.counter(MetricRegistry.name("nuxeo", "repositories", repositoryName, "caches",
"unified", "hits"));
cacheGetTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repositoryName, "caches",
"unified", "get"));
sorRows = registry.counter(MetricRegistry.name("nuxeo", "repositories", repositoryName, "caches", "unified",
"sor", "rows"));
sorGetTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repositoryName, "caches", "unified",
"sor", "get"));
String gaugeName = MetricRegistry.name("nuxeo", "repositories", repositoryName, "caches", "unified",
"cache-size");
SortedMap<String, Gauge> gauges = registry.getGauges();
if (!gauges.containsKey(gaugeName)) {
registry.register(gaugeName, new Gauge<Integer>() {
@Override
public Integer getValue() {
if (cacheManager != null) {
return cacheManager.getCache(CACHE_NAME).getSize();
}
return 0;
}
});
}
}
public void close() {
invalidationsPropagator.removeQueue(invalidationsQueue);
rowMapperCount.decrementAndGet();
}
@Override
public Serializable generateNewId() {
return rowMapper.generateNewId();
}
/*
* ----- ehcache -----
*/
protected boolean hasTransaction() {
TransactionManagerLookup transactionManagerLookup = cache.getTransactionManagerLookup();
if (transactionManagerLookup == null) {
return false;
}
TransactionManager transactionManager = transactionManagerLookup.getTransactionManager();
if (transactionManager == null) {
return false;
}
Transaction transaction;
try {
transaction = transactionManager.getTransaction();
} catch (SystemException e) {
throw new RuntimeException(e);
}
return transaction != null;
}
protected boolean useEhCache() {
return !isXA || hasTransaction();
}
protected void ehCachePut(Element element) {
if (useEhCache()) {
cache.put(element);
}
}
protected Element ehCacheGet(Serializable key) {
if (useEhCache()) {
return cache.get(key);
}
return null;
}
protected int ehCacheGetSize() {
if (useEhCache()) {
return cache.getSize();
}
return 0;
}
protected boolean ehCacheRemove(Serializable key) {
if (useEhCache()) {
return cache.remove(key);
}
return false;
}
protected void ehCacheRemoveAll() {
if (useEhCache()) {
cache.removeAll();
}
}
/*
* ----- Cache -----
*/
protected static boolean isAbsent(Row row) {
return row.tableName == ABSENT; // == is ok
}
protected void cachePut(Row row) {
row = row.clone();
// for ACL collections, make sure the order is correct
// (without the cache, the query to get a list of collection does an
// ORDER BY pos, so users of the cache must get the same behavior)
if (row.isCollection() && row.values.length > 0 && row.values[0] instanceof ACLRow) {
row.values = sortACLRows((ACLRow[]) row.values);
}
Element element = new Element(new RowId(row), row);
ehCachePut(element);
}
protected ACLRow[] sortACLRows(ACLRow[] acls) {
List<ACLRow> list = new ArrayList<ACLRow>(Arrays.asList(acls));
Collections.sort(list, ACLRowPositionComparator.INSTANCE);
ACLRow[] res = new ACLRow[acls.length];
return list.toArray(res);
}
protected void cachePutAbsent(RowId rowId) {
Element element = new Element(new RowId(rowId), new Row(ABSENT, (Serializable) null));
ehCachePut(element);
}
protected void cachePutAbsentIfNull(RowId rowId, Row row) {
if (row != null) {
cachePut(row);
} else {
cachePutAbsent(rowId);
}
}
protected void cachePutAbsentIfRowId(RowId rowId) {
if (rowId instanceof Row) {
cachePut((Row) rowId);
} else {
cachePutAbsent(rowId);
}
}
protected Row cacheGet(RowId rowId) {
final Context context = cacheGetTimer.time();
try {
Element element = ehCacheGet(rowId);
Row row = null;
if (element != null) {
row = (Row) element.getObjectValue();
}
if (row != null && !isAbsent(row)) {
row = row.clone();
}
if (row != null) {
cacheHitCount.inc();
}
return row;
} finally {
context.stop();
}
}
protected void cacheRemove(RowId rowId) {
ehCacheRemove(rowId);
}
/*
* ----- Invalidations / Cache Management -----
*/
@Override
public Invalidations receiveInvalidations() {
// invalidations from the underlying mapper (cluster)
// already propagated to our invalidations queue
Invalidations remoteInvals = rowMapper.receiveInvalidations();
Invalidations ret = invalidationsQueue.getInvalidations();
if (remoteInvals != null) {
if (!ret.all) {
// only handle remote invalidations, the cache is shared and transactional
if (remoteInvals.modified != null) {
for (RowId rowId : remoteInvals.modified) {
cacheRemove(rowId);
}
}
if (remoteInvals.deleted != null) {
for (RowId rowId : remoteInvals.deleted) {
cachePutAbsent(rowId);
}
}
}
}
// invalidate our cache
if (ret.all) {
clearCache();
}
return ret.isEmpty() ? null : ret;
}
// propagate invalidations
@Override
public void sendInvalidations(Invalidations invalidations) {
// add local invalidations
if (!localInvalidations.isEmpty()) {
if (invalidations == null) {
invalidations = new Invalidations();
}
invalidations.add(localInvalidations);
localInvalidations.clear();
}
if (invalidations != null && !invalidations.isEmpty()) {
// send to underlying mapper
rowMapper.sendInvalidations(invalidations);
// queue to other mappers' caches
invalidationsPropagator.propagateInvalidations(invalidations, invalidationsQueue);
}
}
@Override
public void clearCache() {
ehCacheRemoveAll();
localInvalidations.clear();
rowMapper.clearCache();
}
@Override
public void rollback(Xid xid) throws XAException {
try {
rowMapper.rollback(xid);
} finally {
ehCacheRemoveAll();
localInvalidations.clear();
}
}
/*
* ----- Batch -----
*/
/*
* Use those from the cache if available, read from the mapper for the rest.
*/
@Override
public List<? extends RowId> read(Collection<RowId> rowIds, boolean cacheOnly) {
List<RowId> res = new ArrayList<RowId>(rowIds.size());
// find which are in cache, and which not
List<RowId> todo = new LinkedList<RowId>();
for (RowId rowId : rowIds) {
Row row = cacheGet(rowId);
if (row == null) {
if (cacheOnly) {
res.add(new RowId(rowId));
} else {
todo.add(rowId);
}
} else if (isAbsent(row)) {
res.add(new RowId(rowId));
} else {
res.add(row);
}
}
if (!todo.isEmpty()) {
final Context context = sorGetTimer.time();
try {
// ask missing ones to underlying row mapper
List<? extends RowId> fetched = rowMapper.read(todo, cacheOnly);
// add them to the cache
for (RowId rowId : fetched) {
cachePutAbsentIfRowId(rowId);
}
// merge results
res.addAll(fetched);
sorRows.inc(fetched.size());
} finally {
context.stop();
}
}
return res;
}
/*
* Save in the cache then pass all the writes to the mapper.
*/
@Override
public void write(RowBatch batch) {
// we avoid gathering invalidations for a write-only table: fulltext
for (Row row : batch.creates) {
cachePut(row);
if (!Model.FULLTEXT_TABLE_NAME.equals(row.tableName)) {
// we need to send modified invalidations for created
// fragments because other session's ABSENT fragments have
// to be invalidated
localInvalidations.addModified(new RowId(row));
}
}
for (RowUpdate rowu : batch.updates) {
cachePut(rowu.row);
if (!Model.FULLTEXT_TABLE_NAME.equals(rowu.row.tableName)) {
localInvalidations.addModified(new RowId(rowu.row));
}
}
for (RowId rowId : batch.deletes) {
if (rowId instanceof Row) {
throw new AssertionError();
}
cachePutAbsent(rowId);
if (!Model.FULLTEXT_TABLE_NAME.equals(rowId.tableName)) {
localInvalidations.addDeleted(rowId);
}
}
for (RowId rowId : batch.deletesDependent) {
if (rowId instanceof Row) {
throw new AssertionError();
}
cachePutAbsent(rowId);
if (!Model.FULLTEXT_TABLE_NAME.equals(rowId.tableName)) {
localInvalidations.addDeleted(rowId);
}
}
// propagate to underlying mapper
rowMapper.write(batch);
}
/*
* ----- Read -----
*/
@Override
public Row readSimpleRow(RowId rowId) {
Row row = cacheGet(rowId);
if (row == null) {
row = rowMapper.readSimpleRow(rowId);
cachePutAbsentIfNull(rowId, row);
return row;
} else if (isAbsent(row)) {
return null;
} else {
return row;
}
}
@Override
public Map<String, String> getBinaryFulltext(RowId rowId) {
return rowMapper.getBinaryFulltext(rowId);
}
@Override
public Serializable[] readCollectionRowArray(RowId rowId) {
Row row = cacheGet(rowId);
if (row == null) {
Serializable[] array = rowMapper.readCollectionRowArray(rowId);
assert array != null;
row = new Row(rowId.tableName, rowId.id, array);
cachePut(row);
return row.values;
} else if (isAbsent(row)) {
return null;
} else {
return row.values;
}
}
@Override
public List<Row> readSelectionRows(SelectionType selType, Serializable selId, Serializable filter,
Serializable criterion, boolean limitToOne) {
List<Row> rows = rowMapper.readSelectionRows(selType, selId, filter, criterion, limitToOne);
for (Row row : rows) {
cachePut(row);
}
return rows;
}
/*
* ----- Copy -----
*/
@Override
public CopyResult copy(IdWithTypes source, Serializable destParentId, String destName, Row overwriteRow) {
CopyResult result = rowMapper.copy(source, destParentId, destName, overwriteRow);
Invalidations invalidations = result.invalidations;
if (invalidations.modified != null) {
for (RowId rowId : invalidations.modified) {
cacheRemove(rowId);
localInvalidations.addModified(new RowId(rowId));
}
}
if (invalidations.deleted != null) {
for (RowId rowId : invalidations.deleted) {
cacheRemove(rowId);
localInvalidations.addDeleted(rowId);
}
}
return result;
}
@Override
public List<NodeInfo> remove(NodeInfo rootInfo) {
List<NodeInfo> infos = rowMapper.remove(rootInfo);
for (NodeInfo info : infos) {
for (String fragmentName : model.getTypeFragments(new IdWithTypes(info.id, info.primaryType, null))) {
RowId rowId = new RowId(fragmentName, info.id);
cacheRemove(rowId);
localInvalidations.addDeleted(rowId);
}
}
// we only put as absent the root fragment, to avoid polluting the cache
// with lots of absent info. the rest is removed entirely
cachePutAbsent(new RowId(Model.HIER_TABLE_NAME, rootInfo.id));
return infos;
}
@Override
public long getCacheSize() {
// The unified cache is reported by the cache-size gauge
return 0;
}
}