001/* 002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Florent Guillaume 011 */ 012package org.nuxeo.ecm.core.storage.sql; 013 014import java.io.Serializable; 015import java.util.ArrayList; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.List; 019import java.util.Map; 020import java.util.Set; 021 022import org.apache.commons.collections.map.AbstractReferenceMap; 023import org.apache.commons.collections.map.ReferenceMap; 024import org.nuxeo.runtime.metrics.MetricsService; 025 026import com.codahale.metrics.Counter; 027import com.codahale.metrics.MetricRegistry; 028import com.codahale.metrics.SharedMetricRegistries; 029import com.codahale.metrics.Timer; 030 031/** 032 * A {@link SelectionContext} holds information for a set {@link Selection} objects, mostly acting as a cache. 033 * <p> 034 * Some of the information is identical to what's in the database and can be safely be GC'ed, so it lives in a 035 * memory-sensitive map (softMap), otherwise it's moved to a normal map (hardMap) (creation or deletion). 036 */ 037public class SelectionContext { 038 039 private final SelectionType selType; 040 041 private final Serializable criterion; 042 043 private final RowMapper mapper; 044 045 private final PersistenceContext context; 046 047 private final Map<Serializable, Selection> softMap; 048 049 // public because used from unit tests 050 public final Map<Serializable, Selection> hardMap; 051 052 /** 053 * The selections modified in the transaction, that should be propagated as invalidations to other sessions at 054 * post-commit time. 055 */ 056 private final Set<Serializable> modifiedInTransaction; 057 058 // @since 5.7 059 protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 060 061 protected final Counter modifiedInTransactionCount; 062 063 protected final Counter cacheHitCount; 064 065 protected final Timer cacheGetTimer; 066 067 @SuppressWarnings("unchecked") 068 public SelectionContext(SelectionType selType, Serializable criterion, RowMapper mapper, PersistenceContext context) { 069 this.selType = selType; 070 this.criterion = criterion; 071 this.mapper = mapper; 072 this.context = context; 073 softMap = new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.SOFT); 074 hardMap = new HashMap<Serializable, Selection>(); 075 modifiedInTransaction = new HashSet<Serializable>(); 076 modifiedInTransactionCount = registry.counter(MetricRegistry.name("nuxeo", "repositories", 077 context.session.repository.getName(), "caches", "selections", "modified")); 078 cacheHitCount = registry.counter(MetricRegistry.name("nuxeo", "repositories", 079 context.session.repository.getName(), "caches", "selections", "hit")); 080 cacheGetTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", 081 context.session.repository.getName(), "caches", "selections", "get")); 082 } 083 084 public int clearCaches() { 085 // only the soft selections are caches, the others hold info 086 int n = softMap.size(); 087 softMap.clear(); 088 modifiedInTransactionCount.dec(modifiedInTransaction.size()); 089 modifiedInTransaction.clear(); 090 return n; 091 } 092 093 public int getSize() { 094 return softMap == null ? 0 : softMap.size(); 095 } 096 097 /** Gets the proper selection cache. Creates one if missing. */ 098 private Selection getSelection(Serializable selId) { 099 final Timer.Context timerContext = cacheGetTimer.time(); 100 try { 101 Selection selection = softMap.get(selId); 102 if (selection != null) { 103 cacheHitCount.inc(); 104 return selection; 105 } 106 selection = hardMap.get(selId); 107 if (selection != null) { 108 cacheHitCount.inc(); 109 return selection; 110 } 111 } finally { 112 timerContext.stop(); 113 } 114 115 return new Selection(selId, selType.tableName, false, selType.filterKey, context, softMap, hardMap); 116 } 117 118 public boolean applicable(SimpleFragment fragment) { 119 // check table name 120 if (!fragment.row.tableName.equals(selType.tableName)) { 121 return false; 122 } 123 // check criterion if there's one 124 if (selType.criterionKey != null) { 125 Serializable crit = fragment.get(selType.criterionKey); 126 if (!criterion.equals(crit)) { 127 return false; 128 } 129 } 130 return true; 131 } 132 133 /** 134 * Records the fragment as a just-created selection member. 135 */ 136 public void recordCreated(SimpleFragment fragment) { 137 Serializable id = fragment.getId(); 138 // add as a new fragment in the selection 139 Serializable selId = fragment.get(selType.selKey); 140 if (selId != null) { 141 getSelection(selId).addCreated(id); 142 modifiedInTransaction.add(selId); 143 modifiedInTransactionCount.inc(); 144 } 145 } 146 147 /** 148 * Notes that a new empty selection should be created. 149 */ 150 public void newSelection(Serializable selId) { 151 new Selection(selId, selType.tableName, true, selType.filterKey, context, softMap, hardMap); 152 } 153 154 /** 155 * @param invalidate {@code true} if this is for a fragment newly created by internal database process (copy, etc.) 156 * and must notified to other session; {@code false} if this is a normal read 157 */ 158 public void recordExisting(SimpleFragment fragment, boolean invalidate) { 159 Serializable selId = fragment.get(selType.selKey); 160 if (selId != null) { 161 getSelection(selId).addExisting(fragment.getId()); 162 if (invalidate) { 163 modifiedInTransaction.add(selId); 164 modifiedInTransactionCount.inc(); 165 } 166 } 167 } 168 169 /** Removes a selection item from the selection. */ 170 public void recordRemoved(SimpleFragment fragment) { 171 recordRemoved(fragment.getId(), fragment.get(selType.selKey)); 172 } 173 174 /** Removes a selection item from the selection. */ 175 public void recordRemoved(Serializable id, Serializable selId) { 176 if (selId != null) { 177 getSelection(selId).remove(id); 178 modifiedInTransaction.add(selId); 179 modifiedInTransactionCount.inc(); 180 } 181 } 182 183 /** Records a selection as removed. */ 184 public void recordRemovedSelection(Serializable selId) { 185 softMap.remove(selId); 186 hardMap.remove(selId); 187 modifiedInTransaction.add(selId); 188 modifiedInTransactionCount.inc(); 189 } 190 191 /** 192 * Find a fragment given its selection id and value. 193 * <p> 194 * If the fragment is not in the context, fetch it from the mapper. 195 * 196 * @param selId the selection id 197 * @param filter the value to filter on 198 * @return the fragment, or {@code null} if not found 199 */ 200 public SimpleFragment getSelectionFragment(Serializable selId, String filter) { 201 SimpleFragment fragment = getSelection(selId).getFragmentByValue(filter); 202 if (fragment == SimpleFragment.UNKNOWN) { 203 // read it through the mapper 204 List<Row> rows = mapper.readSelectionRows(selType, selId, filter, criterion, true); 205 Row row = rows.isEmpty() ? null : rows.get(0); 206 fragment = (SimpleFragment) context.getFragmentFromFetchedRow(row, false); 207 } 208 return fragment; 209 } 210 211 /** 212 * Finds all the selection fragments for a given id. 213 * <p> 214 * No sorting on value is done. 215 * 216 * @param selId the selection id 217 * @param filter the value to filter on, or {@code null} for all 218 * @return the list of fragments 219 */ 220 public List<SimpleFragment> getSelectionFragments(Serializable selId, String filter) { 221 Selection selection = getSelection(selId); 222 List<SimpleFragment> fragments = selection.getFragmentsByValue(filter); 223 if (fragments == null) { 224 // no complete list is known 225 // ask the actual selection to the mapper 226 List<Row> rows = mapper.readSelectionRows(selType, selId, null, criterion, false); 227 List<Fragment> frags = context.getFragmentsFromFetchedRows(rows, false); 228 fragments = new ArrayList<SimpleFragment>(frags.size()); 229 List<Serializable> ids = new ArrayList<Serializable>(frags.size()); 230 for (Fragment fragment : frags) { 231 fragments.add((SimpleFragment) fragment); 232 ids.add(fragment.getId()); 233 } 234 selection.addExistingComplete(ids); 235 236 // redo the query, as the selection may include newly-created ones, 237 // and we also filter by name 238 fragments = selection.getFragmentsByValue(filter); 239 } 240 return fragments; 241 } 242 243 public void postSave() { 244 // flush selection caches (moves from hard to soft) 245 for (Selection selection : hardMap.values()) { 246 selection.flush(); // added to soft map 247 } 248 hardMap.clear(); 249 } 250 251 /** 252 * Marks locally all the invalidations gathered by a {@link Mapper} operation (like a version restore). 253 */ 254 public void markInvalidated(Set<RowId> modified) { 255 for (RowId rowId : modified) { 256 if (selType.invalidationTableName.equals(rowId.tableName)) { 257 Serializable id = rowId.id; 258 Selection selection = softMap.get(id); 259 if (selection != null) { 260 selection.setIncomplete(); 261 } 262 selection = hardMap.get(id); 263 if (selection != null) { 264 selection.setIncomplete(); 265 } 266 modifiedInTransaction.add(id); 267 modifiedInTransactionCount.inc(); 268 } 269 } 270 } 271 272 /** 273 * Gathers invalidations from this session. 274 * <p> 275 * Called post-transaction to gathers invalidations to be sent to others. 276 */ 277 public void gatherInvalidations(Invalidations invalidations) { 278 for (Serializable id : modifiedInTransaction) { 279 invalidations.addModified(new RowId(selType.invalidationTableName, id)); 280 } 281 modifiedInTransactionCount.dec(modifiedInTransaction.size()); 282 modifiedInTransaction.clear(); 283 } 284 285 /** 286 * Processes all invalidations accumulated. 287 * <p> 288 * Called pre-transaction. 289 */ 290 public void processReceivedInvalidations(Set<RowId> modified) { 291 for (RowId rowId : modified) { 292 if (selType.invalidationTableName.equals(rowId.tableName)) { 293 Serializable id = rowId.id; 294 softMap.remove(id); 295 hardMap.remove(id); 296 } 297 } 298 } 299 300}