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}