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