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