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}