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