/* * (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 */ 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.nuxeo.ecm.core.storage.sql.ACLRow.ACLRowPositionComparator; 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 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; /** * 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); } public void initialize(String repositoryName, Model model, RowMapper rowMapper, InvalidationsPropagator cachePropagator, Map<String, String> properties) { this.model = model; this.rowMapper = rowMapper; this.cachePropagator = cachePropagator; cachePropagator.addQueue(cacheQueue); setMetrics(repositoryName); } protected void setMetrics(String repositoryName) { cacheHitCount = registry.counter(MetricRegistry.name("nuxeo", "repositories", repositoryName, "caches", "soft-ref", "hits")); cacheGetTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repositoryName, "caches", "soft-ref", "get")); sorRows = registry.counter(MetricRegistry.name("nuxeo", "repositories", repositoryName, "caches", "soft-ref", "sor", "rows")); sorGetTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repositoryName, "caches", "soft-ref", "sor", "get")); } public void close() { clearCache(); cachePropagator.removeQueue(cacheQueue); } @Override public Serializable generateNewId() { 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 Invalidations receiveInvalidations() { // invalidations from the underlying mapper (cluster) // already propagated to our invalidations queue rowMapper.receiveInvalidations(); Invalidations invalidations = cacheQueue.getInvalidations(); // 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); } } return invalidations.isEmpty() ? null : invalidations; } // 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 local mappers' caches cachePropagator.propagateInvalidations(invalidations, cacheQueue); } } @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) { 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) { 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) { 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; } }