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