/* * (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.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.collections.map.AbstractReferenceMap; import org.apache.commons.collections.map.ReferenceMap; 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 SelectionContext} holds information for a set {@link Selection} objects, mostly acting as a cache. * <p> * Some of the information is identical to what's in the database and can be safely be GC'ed, so it lives in a * memory-sensitive map (softMap), otherwise it's moved to a normal map (hardMap) (creation or deletion). */ public class SelectionContext { private final SelectionType selType; private final Serializable criterion; private final RowMapper mapper; private final PersistenceContext context; private final Map<Serializable, Selection> softMap; // public because used from unit tests public final Map<Serializable, Selection> hardMap; /** * The selections modified in the transaction, that should be propagated as invalidations to other sessions at * post-commit time. */ private final Set<Serializable> modifiedInTransaction; // @since 5.7 protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); protected final Counter modifiedInTransactionCount; protected final Counter cacheHitCount; protected final Timer cacheGetTimer; @SuppressWarnings("unchecked") public SelectionContext(SelectionType selType, Serializable criterion, RowMapper mapper, PersistenceContext context) { this.selType = selType; this.criterion = criterion; this.mapper = mapper; this.context = context; softMap = new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.SOFT); hardMap = new HashMap<Serializable, Selection>(); modifiedInTransaction = new HashSet<Serializable>(); modifiedInTransactionCount = registry.counter(MetricRegistry.name("nuxeo", "repositories", context.session.repository.getName(), "caches", "selections", "modified")); cacheHitCount = registry.counter(MetricRegistry.name("nuxeo", "repositories", context.session.repository.getName(), "caches", "selections", "hit")); cacheGetTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", context.session.repository.getName(), "caches", "selections", "get")); } public int clearCaches() { // only the soft selections are caches, the others hold info int n = softMap.size(); softMap.clear(); modifiedInTransactionCount.dec(modifiedInTransaction.size()); modifiedInTransaction.clear(); return n; } public int getSize() { return softMap == null ? 0 : softMap.size(); } /** Gets the proper selection cache. Creates one if missing. */ private Selection getSelection(Serializable selId) { final Timer.Context timerContext = cacheGetTimer.time(); try { Selection selection = softMap.get(selId); if (selection != null) { cacheHitCount.inc(); return selection; } selection = hardMap.get(selId); if (selection != null) { cacheHitCount.inc(); return selection; } } finally { timerContext.stop(); } return new Selection(selId, selType.tableName, false, selType.filterKey, context, softMap, hardMap); } public boolean applicable(SimpleFragment fragment) { // check table name if (!fragment.row.tableName.equals(selType.tableName)) { return false; } // check criterion if there's one if (selType.criterionKey != null) { Serializable crit = fragment.get(selType.criterionKey); if (!criterion.equals(crit)) { return false; } } return true; } /** * Records the fragment as a just-created selection member. */ public void recordCreated(SimpleFragment fragment) { Serializable id = fragment.getId(); // add as a new fragment in the selection Serializable selId = fragment.get(selType.selKey); if (selId != null) { getSelection(selId).addCreated(id); modifiedInTransaction.add(selId); modifiedInTransactionCount.inc(); } } /** * Notes that a new empty selection should be created. */ public void newSelection(Serializable selId) { new Selection(selId, selType.tableName, true, selType.filterKey, context, softMap, hardMap); } /** * @param invalidate {@code true} if this is for a fragment newly created by internal database process (copy, etc.) * and must notified to other session; {@code false} if this is a normal read */ public void recordExisting(SimpleFragment fragment, boolean invalidate) { Serializable selId = fragment.get(selType.selKey); if (selId != null) { getSelection(selId).addExisting(fragment.getId()); if (invalidate) { modifiedInTransaction.add(selId); modifiedInTransactionCount.inc(); } } } /** Removes a selection item from the selection. */ public void recordRemoved(SimpleFragment fragment) { recordRemoved(fragment.getId(), fragment.get(selType.selKey)); } /** Removes a selection item from the selection. */ public void recordRemoved(Serializable id, Serializable selId) { if (selId != null) { getSelection(selId).remove(id); modifiedInTransaction.add(selId); modifiedInTransactionCount.inc(); } } /** Records a selection as removed. */ public void recordRemovedSelection(Serializable selId) { softMap.remove(selId); hardMap.remove(selId); modifiedInTransaction.add(selId); modifiedInTransactionCount.inc(); } /** * Find a fragment given its selection id and value. * <p> * If the fragment is not in the context, fetch it from the mapper. * * @param selId the selection id * @param filter the value to filter on * @return the fragment, or {@code null} if not found */ public SimpleFragment getSelectionFragment(Serializable selId, String filter) { SimpleFragment fragment = getSelection(selId).getFragmentByValue(filter); if (fragment == SimpleFragment.UNKNOWN) { // read it through the mapper List<Row> rows = mapper.readSelectionRows(selType, selId, filter, criterion, true); Row row = rows.isEmpty() ? null : rows.get(0); fragment = (SimpleFragment) context.getFragmentFromFetchedRow(row, false); } return fragment; } /** * Finds all the selection fragments for a given id. * <p> * No sorting on value is done. * * @param selId the selection id * @param filter the value to filter on, or {@code null} for all * @return the list of fragments */ public List<SimpleFragment> getSelectionFragments(Serializable selId, String filter) { Selection selection = getSelection(selId); List<SimpleFragment> fragments = selection.getFragmentsByValue(filter); if (fragments == null) { // no complete list is known // ask the actual selection to the mapper List<Row> rows = mapper.readSelectionRows(selType, selId, null, criterion, false); List<Fragment> frags = context.getFragmentsFromFetchedRows(rows, false); fragments = new ArrayList<SimpleFragment>(frags.size()); List<Serializable> ids = new ArrayList<Serializable>(frags.size()); for (Fragment fragment : frags) { fragments.add((SimpleFragment) fragment); ids.add(fragment.getId()); } selection.addExistingComplete(ids); // redo the query, as the selection may include newly-created ones, // and we also filter by name fragments = selection.getFragmentsByValue(filter); } return fragments; } public void postSave() { // flush selection caches (moves from hard to soft) for (Selection selection : hardMap.values()) { selection.flush(); // added to soft map } hardMap.clear(); } /** * Marks locally all the invalidations gathered by a {@link Mapper} operation (like a version restore). */ public void markInvalidated(Set<RowId> modified) { for (RowId rowId : modified) { if (selType.invalidationTableName.equals(rowId.tableName)) { Serializable id = rowId.id; Selection selection = softMap.get(id); if (selection != null) { selection.setIncomplete(); } selection = hardMap.get(id); if (selection != null) { selection.setIncomplete(); } modifiedInTransaction.add(id); modifiedInTransactionCount.inc(); } } } /** * Gathers invalidations from this session. * <p> * Called post-transaction to gathers invalidations to be sent to others. */ public void gatherInvalidations(Invalidations invalidations) { for (Serializable id : modifiedInTransaction) { invalidations.addModified(new RowId(selType.invalidationTableName, id)); } modifiedInTransactionCount.dec(modifiedInTransaction.size()); modifiedInTransaction.clear(); } /** * Processes all invalidations accumulated. * <p> * Called pre-transaction. */ public void processReceivedInvalidations(Set<RowId> modified) { for (RowId rowId : modified) { if (selType.invalidationTableName.equals(rowId.tableName)) { Serializable id = rowId.id; softMap.remove(id); hardMap.remove(id); } } } }