/*
* Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Florent Guillaume
*/
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 javax.transaction.xa.XAException;
import javax.transaction.xa.Xid;
import org.apache.commons.collections.map.AbstractReferenceMap;
import org.apache.commons.collections.map.ReferenceMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.storage.StorageException;
import org.nuxeo.ecm.core.storage.sql.ACLRow.ACLRowPositionComparator;
import org.nuxeo.ecm.core.storage.sql.Invalidations.InvalidationsPair;
import org.nuxeo.runtime.metrics.MetricsService;
import com.codahale.metrics.Counter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
/**
* A {@link RowMapper} that has an internal cache.
* <p>
* The cache only holds {@link Row}s that are known to be identical to what's in
* the underlying {@link RowMapper}.
*/
public class SoftRefCachingRowMapper implements RowMapper {
private static final Log log = LogFactory.getLog(SoftRefCachingRowMapper.class);
private static final String ABSENT = "__ABSENT__\0\0\0";
/**
* The cached rows. All held data is identical to what is present in the
* underlying {@link RowMapper} and could be refetched if needed.
* <p>
* The values are either {@link Row} for fragments present in the database,
* or a row with tableName {@link #ABSENT} to denote a fragment known to be
* absent from the database.
* <p>
* This cache is memory-sensitive (all values are soft-referenced), a
* fragment can always be refetched if the GC collects it.
*/
// we use a new Row instance for the absent case to avoid keeping other
// references to it which would prevent its GCing
private final Map<RowId, Row> 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 cache invalidations received from other session, to process
* at pre-transaction time.
*/
// public for unit tests
public final InvalidationsQueue cacheQueue;
/**
* The propagator of invalidations to other mappers.
*/
private InvalidationsPropagator cachePropagator;
/**
* The queue of invalidations used for events, a single queue is shared by
* all mappers corresponding to the same client repository.
*/
private InvalidationsQueue eventQueue;
/**
* The propagator of event invalidations to all event queues.
*/
private InvalidationsPropagator eventPropagator;
/**
* The session, used for event propagation.
*/
private SessionImpl session;
protected boolean forRemoteClient;
/**
* 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;
@SuppressWarnings("unchecked")
public SoftRefCachingRowMapper() {
cache = new ReferenceMap(AbstractReferenceMap.HARD,
AbstractReferenceMap.SOFT);
localInvalidations = new Invalidations();
cacheQueue = new InvalidationsQueue("mapper-" + this);
forRemoteClient = false;
}
public void initialize(Model model, RowMapper rowMapper,
InvalidationsPropagator cachePropagator,
InvalidationsPropagator eventPropagator,
InvalidationsQueue repositoryEventQueue,
Map<String, String> properties) {
this.model = model;
this.rowMapper = rowMapper;
this.cachePropagator = cachePropagator;
cachePropagator.addQueue(cacheQueue);
eventQueue = repositoryEventQueue;
this.eventPropagator = eventPropagator;
eventPropagator.addQueue(repositoryEventQueue);
}
public void close() throws StorageException {
clearCache();
cachePropagator.removeQueue(cacheQueue);
eventPropagator.removeQueue(eventQueue); // TODO can be overriden
}
@Override
public Serializable generateNewId() throws StorageException {
return rowMapper.generateNewId();
}
/*
* ----- 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);
}
cache.put(new RowId(row), row);
}
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) {
cache.put(new RowId(rowId), new Row(ABSENT, (Serializable) null));
}
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 Timer.Context context = cacheGetTimer.time();
try {
Row row = cache.get(rowId);
if (row != null && !isAbsent(row)) {
row = row.clone();
}
if (row != null) {
cacheHitCount.inc();
}
return row;
} finally {
context.stop();
}
}
protected void cacheRemove(RowId rowId) {
cache.remove(rowId);
}
/*
* ----- Invalidations / Cache Management -----
*/
@Override
public InvalidationsPair receiveInvalidations() throws StorageException {
// invalidations from the underlying mapper (remote, cluster)
InvalidationsPair invals = rowMapper.receiveInvalidations();
// add local accumulated invalidations to remote ones
Invalidations invalidations = cacheQueue.getInvalidations();
if (invals != null) {
invalidations.add(invals.cacheInvalidations);
}
// add local accumulated events to remote ones
Invalidations events = eventQueue.getInvalidations();
if (invals != null) {
events.add(invals.eventInvalidations);
}
// invalidate our cache
if (invalidations.all) {
clearCache();
}
if (invalidations.modified != null) {
for (RowId rowId : invalidations.modified) {
cacheRemove(rowId);
}
}
if (invalidations.deleted != null) {
for (RowId rowId : invalidations.deleted) {
cachePutAbsent(rowId);
}
}
if (invalidations.isEmpty() && events.isEmpty()) {
return null;
}
return new InvalidationsPair(invalidations.isEmpty() ? null
: invalidations, events.isEmpty() ? null : events);
}
// propagate invalidations
@Override
public void sendInvalidations(Invalidations invalidations)
throws StorageException {
// 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 local mappers' caches
cachePropagator.propagateInvalidations(invalidations, cacheQueue);
// queue as events for other repositories
eventPropagator.propagateInvalidations(invalidations, eventQueue);
// send event to local repository (synchronous)
// only if not the server-side part of a remote client
if (!forRemoteClient) {
session.sendInvalidationEvent(invalidations, true);
}
}
}
/**
* Used by the server to associate each mapper to a single event
* invalidations queue per client repository.
*/
public void setEventQueue(InvalidationsQueue eventQueue) {
// don't remove the original global repository queue
this.eventQueue = eventQueue;
eventPropagator.addQueue(eventQueue);
forRemoteClient = true;
}
/**
* Sets the session, used for event propagation.
*/
public void setSession(SessionImpl session) {
this.session = session;
cacheHitCount = registry.counter(MetricRegistry.name(
"nuxeo", "repositories", session.repository.getName(), "caches", "soft-ref", "hits"));
cacheGetTimer = registry.timer(MetricRegistry.name(
"nuxeo", "repositories", session.repository.getName(), "caches", "soft-ref", "get"));
sorRows = registry.counter(MetricRegistry.name(
"nuxeo", "repositories", session.repository.getName(), "caches", "soft-ref", "sor", "rows"));
sorGetTimer = registry.timer(MetricRegistry.name(
"nuxeo", "repositories", session.repository.getName(), "caches", "soft-ref", "sor", "get"));
}
@Override
public void clearCache() {
cache.clear();
sorRows.dec(sorRows.getCount());
localInvalidations.clear();
rowMapper.clearCache();
}
@Override
public long getCacheSize() {
return cache.size();
}
@Override
public void rollback(Xid xid) throws XAException {
try {
rowMapper.rollback(xid);
} finally {
clearCache();
}
}
/*
* ----- 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) throws StorageException {
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 Timer.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) throws StorageException {
for (Row row : batch.creates) {
cachePut(row);
// 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);
localInvalidations.addModified(new RowId(rowu.row));
}
for (RowId rowId : batch.deletes) {
if (rowId instanceof Row) {
throw new AssertionError();
}
cachePutAbsent(rowId);
localInvalidations.addDeleted(rowId);
}
for (RowId rowId : batch.deletesDependent) {
if (rowId instanceof Row) {
throw new AssertionError();
}
cachePutAbsent(rowId);
localInvalidations.addDeleted(rowId);
}
// propagate to underlying mapper
rowMapper.write(batch);
}
/*
* ----- Read -----
*/
@Override
public Row readSimpleRow(RowId rowId) throws StorageException {
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) throws StorageException {
return rowMapper.getBinaryFulltext(rowId);
}
@Override
public Serializable[] readCollectionRowArray(RowId rowId)
throws StorageException {
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) throws StorageException {
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) throws StorageException {
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) throws StorageException {
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;
}
}