001/* 002 * (C) Copyright 2007-2014 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 * Maxime Hilaire 018 */ 019 020package org.nuxeo.ecm.directory; 021 022import java.io.Serializable; 023import java.util.Arrays; 024import java.util.List; 025 026import org.apache.commons.logging.Log; 027import org.apache.commons.logging.LogFactory; 028import org.nuxeo.ecm.core.api.DocumentModel; 029import org.nuxeo.ecm.core.api.NuxeoException; 030import org.nuxeo.ecm.core.cache.Cache; 031import org.nuxeo.ecm.core.cache.CacheService; 032import org.nuxeo.runtime.api.Framework; 033import org.nuxeo.runtime.metrics.MetricsService; 034 035import com.codahale.metrics.Counter; 036import com.codahale.metrics.MetricRegistry; 037import com.codahale.metrics.SharedMetricRegistries; 038 039/** 040 * Very simple cache system to cache directory entry lookups (not search queries) on top of nuxeo cache 041 * <p> 042 * Beware that this cache is not transaction aware (which is not a problem for LDAP directories anyway). 043 */ 044public class DirectoryCache { 045 046 private static final Serializable CACHE_MISS = Boolean.FALSE; 047 048 protected final String name; 049 050 protected Cache entryCache; 051 052 protected String entryCacheName = null; 053 054 protected Cache entryCacheWithoutReferences; 055 056 protected String entryCacheWithoutReferencesName = null; 057 058 protected boolean negativeCaching; 059 060 protected final MetricRegistry metrics = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 061 062 protected final Counter hitsCounter; 063 064 protected final Counter negativeHitsCounter; 065 066 protected final Counter missesCounter; 067 068 protected final Counter invalidationsCounter; 069 070 protected final Counter sizeCounter; 071 072 private final static Log log = LogFactory.getLog(DirectoryCache.class); 073 074 protected DirectoryCache(String name) { 075 this.name = name; 076 hitsCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "hits")); 077 negativeHitsCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "neghits")); 078 missesCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "misses")); 079 invalidationsCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", 080 "invalidations")); 081 sizeCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "size")); 082 } 083 084 protected boolean isCacheEnabled() { 085 return (entryCacheName != null && entryCacheWithoutReferencesName != null); 086 } 087 088 public DocumentModel getEntry(String entryId, EntrySource source) throws DirectoryException { 089 return getEntry(entryId, source, true); 090 } 091 092 public DocumentModel getEntry(String entryId, EntrySource source, boolean fetchReferences) 093 throws DirectoryException { 094 if (!isCacheEnabled()) { 095 return source.getEntryFromSource(entryId, fetchReferences); 096 } else if (isCacheEnabled() && (getEntryCache() == null || getEntryCacheWithoutReferences() == null)) { 097 if (log.isDebugEnabled()) { 098 if (getEntryCache() == null) { 099 log.debug(String.format( 100 "The cache '%s' is undefined for directory '%s', it will be created with the default cache configuration", 101 entryCacheName, name)); 102 } 103 if (getEntryCacheWithoutReferences() == null) { 104 log.debug(String.format( 105 "The cache '%s' is undefined for directory '%s', it will be created with the default cache configuration", 106 entryCacheWithoutReferencesName, name)); 107 } 108 } 109 return source.getEntryFromSource(entryId, fetchReferences); 110 } 111 112 Cache cache = fetchReferences ? getEntryCache() : getEntryCacheWithoutReferences(); 113 Serializable entry = cache.get(entryId); 114 if (CACHE_MISS.equals(entry)) { 115 negativeHitsCounter.inc(); 116 return null; 117 } 118 DocumentModel dm = (DocumentModel) entry; 119 if (dm == null) { 120 // fetch the entry from the backend and cache it for later reuse 121 dm = source.getEntryFromSource(entryId, fetchReferences); 122 if (dm != null) { 123 // DocumentModelImpl is not thread-safe and when we fetch and clone it when returning 124 // a value from the cache there may be concurrency. 125 // So we avoid thread-safety issues by exercising once the code paths that may do 126 // concurrent accesses to ComplexProperty (NXP-23458). 127 try { 128 dm.clone(); 129 } catch (CloneNotSupportedException e) { 130 // ignore, no concurrency issues if not a DocumentModelImpl 131 } 132 cache.put(entryId, dm); 133 if (fetchReferences) { 134 sizeCounter.inc(); 135 } 136 } else if (negativeCaching) { 137 cache.put(entryId, CACHE_MISS); 138 } 139 missesCounter.inc(); 140 } else { 141 hitsCounter.inc(); 142 } 143 try { 144 if (dm == null) { 145 return null; 146 } 147 // this is the clone() that needs to be careful (see above) when there's concurrency 148 DocumentModel clone = dm.clone(); 149 // DocumentModelImpl#clone does not copy context data, hence 150 // propagate the read-only flag manually 151 if (BaseSession.isReadOnlyEntry(dm)) { 152 BaseSession.setReadOnlyEntry(clone); 153 } 154 return clone; 155 } catch (CloneNotSupportedException e) { 156 // will never happen as long a DocumentModelImpl is used 157 return dm; 158 } 159 } 160 161 public void invalidate(List<String> entryIds) { 162 if (isCacheEnabled()) { 163 synchronized (this) { 164 for (String entryId : entryIds) { 165 sizeCounter.dec(); 166 invalidationsCounter.inc(); 167 // caches may be null if we're called for invalidation during a hot-reload 168 Cache cache = getEntryCache(); 169 if (cache != null) { 170 cache.invalidate(entryId); 171 } 172 cache = getEntryCacheWithoutReferences(); 173 if (cache != null) { 174 cache.invalidate(entryId); 175 } 176 } 177 } 178 } 179 } 180 181 public void invalidate(String... entryIds) { 182 invalidate(Arrays.asList(entryIds)); 183 } 184 185 public void invalidateAll() { 186 if (isCacheEnabled()) { 187 synchronized (this) { 188 long count = sizeCounter.getCount(); 189 sizeCounter.dec(count); 190 invalidationsCounter.inc(count); 191 // caches may be null if we're called for invalidation during a hot-reload 192 Cache cache = getEntryCache(); 193 if (cache != null) { 194 cache.invalidateAll(); 195 } 196 cache = getEntryCacheWithoutReferences(); 197 if (cache != null) { 198 cache.invalidateAll(); 199 } 200 } 201 } 202 } 203 204 public void setEntryCacheName(String entryCacheName) { 205 this.entryCacheName = entryCacheName; 206 } 207 208 public void setEntryCacheWithoutReferencesName(String entryCacheWithoutReferencesName) { 209 this.entryCacheWithoutReferencesName = entryCacheWithoutReferencesName; 210 } 211 212 public void setNegativeCaching(Boolean negativeCaching) { 213 this.negativeCaching = Boolean.TRUE.equals(negativeCaching); 214 } 215 216 public Cache getEntryCache() { 217 if (entryCache == null) { 218 entryCache = getCacheService().getCache(entryCacheName); 219 } 220 return entryCache; 221 } 222 223 public Cache getEntryCacheWithoutReferences() { 224 225 if (entryCacheWithoutReferences == null) { 226 entryCacheWithoutReferences = getCacheService().getCache( 227 entryCacheWithoutReferencesName); 228 } 229 return entryCacheWithoutReferences; 230 } 231 232 protected CacheService getCacheService() { 233 CacheService cacheService = Framework.getService(CacheService.class); 234 if (cacheService == null) { 235 throw new NuxeoException("Missing CacheService"); 236 } 237 return cacheService; 238 } 239 240}