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 Selection selection = getSelectionOrNull(selId); 107 if (selection != null) { 108 return selection; 109 } 110 return new Selection(selId, selType.tableName, false, selType.filterKey, context, softMap, hardMap); 111 } 112 113 /** 114 * Gets the proper selection cache, if it exists, otherwise returns {@code null}. 115 * 116 * @since 9.2 117 */ 118 protected Selection getSelectionOrNull(Serializable selId) { 119 final Timer.Context timerContext = cacheGetTimer.time(); 120 try { 121 Selection selection = softMap.get(selId); 122 if (selection != null) { 123 cacheHitCount.inc(); 124 return selection; 125 } 126 selection = hardMap.get(selId); 127 if (selection != null) { 128 cacheHitCount.inc(); 129 return selection; 130 } 131 } finally { 132 timerContext.stop(); 133 } 134 return null; 135 } 136 137 public boolean applicable(SimpleFragment fragment) { 138 // check table name 139 if (!fragment.row.tableName.equals(selType.tableName)) { 140 return false; 141 } 142 // check criterion if there's one 143 if (selType.criterionKey != null) { 144 Serializable crit = fragment.get(selType.criterionKey); 145 if (!criterion.equals(crit)) { 146 return false; 147 } 148 } 149 return true; 150 } 151 152 /** 153 * Records the fragment as a just-created selection member. 154 */ 155 public void recordCreated(SimpleFragment fragment) { 156 Serializable id = fragment.getId(); 157 // add as a new fragment in the selection 158 Serializable selId = fragment.get(selType.selKey); 159 if (selId != null) { 160 getSelection(selId).addCreated(id); 161 modifiedInTransaction.add(selId); 162 modifiedInTransactionCount.inc(); 163 } 164 } 165 166 /** 167 * Notes that a new empty selection should be created. 168 */ 169 public void newSelection(Serializable selId) { 170 new Selection(selId, selType.tableName, true, selType.filterKey, context, softMap, hardMap); 171 } 172 173 /** 174 * @param invalidate {@code true} if this is for a fragment newly created by internal database process (copy, etc.) 175 * and must notified to other session; {@code false} if this is a normal read 176 */ 177 public void recordExisting(SimpleFragment fragment, boolean invalidate) { 178 Serializable selId = fragment.get(selType.selKey); 179 if (selId != null) { 180 getSelection(selId).addExisting(fragment.getId()); 181 if (invalidate) { 182 modifiedInTransaction.add(selId); 183 modifiedInTransactionCount.inc(); 184 } 185 } 186 } 187 188 /** Removes a selection item from the selection. */ 189 public void recordRemoved(SimpleFragment fragment) { 190 recordRemoved(fragment.getId(), fragment.get(selType.selKey)); 191 } 192 193 /** Removes a selection item from the selection. */ 194 public void recordRemoved(Serializable id, Serializable selId) { 195 if (selId != null) { 196 getSelection(selId).remove(id); 197 modifiedInTransaction.add(selId); 198 modifiedInTransactionCount.inc(); 199 } 200 } 201 202 /** Records a selection as removed. */ 203 public void recordRemovedSelection(Serializable selId) { 204 softMap.remove(selId); 205 hardMap.remove(selId); 206 modifiedInTransaction.add(selId); 207 modifiedInTransactionCount.inc(); 208 } 209 210 /** 211 * Find a fragment given its selection id and value. 212 * <p> 213 * If the fragment is not in the context, fetch it from the mapper. 214 * 215 * @param selId the selection id 216 * @param filter the value to filter on 217 * @return the fragment, or {@code null} if not found 218 */ 219 public SimpleFragment getSelectionFragment(Serializable selId, String filter) { 220 SimpleFragment fragment = getSelection(selId).getFragmentByValue(filter); 221 if (fragment == SimpleFragment.UNKNOWN) { 222 // read it through the mapper 223 List<Row> rows = mapper.readSelectionRows(selType, selId, filter, criterion, true); 224 Row row = rows.isEmpty() ? null : rows.get(0); 225 fragment = (SimpleFragment) context.getFragmentFromFetchedRow(row, false); 226 } 227 return fragment; 228 } 229 230 /** 231 * Finds all the selection fragments for a given id. 232 * <p> 233 * No sorting on value is done. 234 * 235 * @param selId the selection id 236 * @param filter the value to filter on, or {@code null} for all 237 * @return the list of fragments 238 */ 239 public List<SimpleFragment> getSelectionFragments(Serializable selId, String filter) { 240 Selection selection = getSelection(selId); 241 List<SimpleFragment> fragments = selection.getFragmentsByValue(filter); 242 if (fragments == null) { 243 // no complete list is known 244 // ask the actual selection to the mapper 245 List<Row> rows = mapper.readSelectionRows(selType, selId, null, criterion, false); 246 List<Fragment> frags = context.getFragmentsFromFetchedRows(rows, false); 247 fragments = new ArrayList<SimpleFragment>(frags.size()); 248 List<Serializable> ids = new ArrayList<Serializable>(frags.size()); 249 for (Fragment fragment : frags) { 250 fragments.add((SimpleFragment) fragment); 251 ids.add(fragment.getId()); 252 } 253 selection.addExistingComplete(ids); 254 255 // redo the query, as the selection may include newly-created ones, 256 // and we also filter by name 257 fragments = selection.getFragmentsByValue(filter); 258 } 259 return fragments; 260 } 261 262 /** 263 * Gets all the selection fragment ids for a given list of values. 264 * 265 * @since 9.2 266 */ 267 public Set<Serializable> getSelectionIds(List<Serializable> values) { 268 return mapper.readSelectionsIds(selType, values); 269 } 270 271 public void postSave() { 272 // flush selection caches (moves from hard to soft) 273 for (Selection selection : hardMap.values()) { 274 selection.flush(); // added to soft map 275 } 276 hardMap.clear(); 277 } 278 279 /** 280 * Marks locally all the invalidations gathered by a {@link Mapper} operation (like a version restore). 281 */ 282 public void markInvalidated(Set<RowId> modified) { 283 for (RowId rowId : modified) { 284 if (selType.invalidationTableName.equals(rowId.tableName)) { 285 Serializable id = rowId.id; 286 Selection selection = softMap.get(id); 287 if (selection != null) { 288 selection.setIncomplete(); 289 } 290 selection = hardMap.get(id); 291 if (selection != null) { 292 selection.setIncomplete(); 293 } 294 modifiedInTransaction.add(id); 295 modifiedInTransactionCount.inc(); 296 } 297 } 298 } 299 300 /** 301 * Gathers invalidations from this session. 302 * <p> 303 * Called post-transaction to gathers invalidations to be sent to others. 304 */ 305 public void gatherInvalidations(Invalidations invalidations) { 306 for (Serializable id : modifiedInTransaction) { 307 invalidations.addModified(new RowId(selType.invalidationTableName, id)); 308 } 309 modifiedInTransactionCount.dec(modifiedInTransaction.size()); 310 modifiedInTransaction.clear(); 311 } 312 313 /** 314 * Processes all invalidations accumulated. 315 * <p> 316 * Called pre-transaction. 317 */ 318 public void processReceivedInvalidations(Set<RowId> modified) { 319 for (RowId rowId : modified) { 320 if (selType.invalidationTableName.equals(rowId.tableName)) { 321 Serializable id = rowId.id; 322 softMap.remove(id); 323 hardMap.remove(id); 324 } 325 } 326 } 327 328}