add --shared
[pylucene.git] / lucene-java-3.4.0 / lucene / contrib / facet / src / java / org / apache / lucene / facet / search / TotalFacetCountsCache.java
1 package org.apache.lucene.facet.search;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.util.Iterator;
6 import java.util.LinkedHashMap;
7 import java.util.concurrent.ConcurrentHashMap;
8 import java.util.concurrent.ConcurrentLinkedQueue;
9
10 import org.apache.lucene.index.IndexReader;
11
12 import org.apache.lucene.facet.index.params.CategoryListParams;
13 import org.apache.lucene.facet.index.params.FacetIndexingParams;
14 import org.apache.lucene.facet.search.cache.CategoryListCache;
15 import org.apache.lucene.facet.taxonomy.TaxonomyReader;
16
17 /**
18  * Licensed to the Apache Software Foundation (ASF) under one or more
19  * contributor license agreements.  See the NOTICE file distributed with
20  * this work for additional information regarding copyright ownership.
21  * The ASF licenses this file to You under the Apache License, Version 2.0
22  * (the "License"); you may not use this file except in compliance with
23  * the License.  You may obtain a copy of the License at
24  *
25  *     http://www.apache.org/licenses/LICENSE-2.0
26  *
27  * Unless required by applicable law or agreed to in writing, software
28  * distributed under the License is distributed on an "AS IS" BASIS,
29  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30  * See the License for the specific language governing permissions and
31  * limitations under the License.
32  */
33
34 /**
35  * Manage an LRU cache for {@link TotalFacetCounts} per index, taxonomy, and
36  * facet indexing params.
37  * 
38  * @lucene.experimental
39  */
40 public final class TotalFacetCountsCache {
41   
42   /**
43    * Default size of in memory cache for computed total facet counts.
44    * Set to 2 for the case when an application reopened a reader and 
45    * the original one is still in use (Otherwise there will be 
46    * switching again and again between the two.) 
47    */
48   public static final int DEFAULT_CACHE_SIZE = 2; 
49
50   private static final TotalFacetCountsCache singleton = new TotalFacetCountsCache();
51   
52   /**
53    * Get the single instance of this cache
54    */
55   public static TotalFacetCountsCache getSingleton() {
56     return singleton;
57   }
58   
59   /**
60    * In-memory cache of TFCs.
61    * <ul>  
62    * <li>It's size is kept within limits through {@link #trimCache()}.
63    * <li>An LRU eviction policy is applied, by maintaining active keys in {@link #lruKeys}. 
64    * <li>After each addition to the cache, trimCache is called, to remove entries least recently used.
65    * </ul>  
66    * @see #markRecentlyUsed(TFCKey)
67    */
68   private ConcurrentHashMap<TFCKey,TotalFacetCounts> cache = new ConcurrentHashMap<TFCKey,TotalFacetCounts>();
69   
70   /**
71    * A queue of active keys for applying LRU policy on eviction from the {@link #cache}.
72    * @see #markRecentlyUsed(TFCKey)
73    */
74   private ConcurrentLinkedQueue<TFCKey> lruKeys = new ConcurrentLinkedQueue<TFCKey>();
75   
76   private int maxCacheSize = DEFAULT_CACHE_SIZE; 
77   
78   /** private constructor for singleton pattern */ 
79   private TotalFacetCountsCache() {
80   }
81   
82   /**
83    * Get the total facet counts for a reader/taxonomy pair and facet indexing parameters.
84    * If not in cache, computed here and added to the cache for later use.
85    * @param indexReader the documents index
86    * @param taxonomy the taxonomy index
87    * @param facetIndexingParams facet indexing parameters
88    * @param clCache category list cache for faster computation, can be null 
89    * @return the total facet counts.
90    */
91   public TotalFacetCounts getTotalCounts(IndexReader indexReader, TaxonomyReader taxonomy,
92       FacetIndexingParams facetIndexingParams, CategoryListCache clCache) throws IOException {
93     // create the key
94     TFCKey key = new TFCKey(indexReader, taxonomy, facetIndexingParams);
95     // it is important that this call is not synchronized, so that available TFC 
96     // would not wait for one that needs to be computed.  
97     TotalFacetCounts tfc = cache.get(key);
98     if (tfc != null) {
99       markRecentlyUsed(key); 
100       return tfc;
101     }
102     return computeAndCache(key, clCache);
103   }
104
105   /**
106    * Mark key as it as recently used.
107    * <p>
108    * <b>Implementation notes: Synchronization considerations and the interaction between lruKeys and cache:</b>
109    * <ol>
110    *  <li>A concurrent {@link LinkedHashMap} would have made this class much simpler.
111    *      But unfortunately, Java does not provide one.
112    *      Instead, we combine two concurrent objects:
113    *  <ul>
114    *   <li>{@link ConcurrentHashMap} for the cached TFCs.
115    *   <li>{@link ConcurrentLinkedQueue} for active keys
116    *  </ul>
117    *  <li>Both {@link #lruKeys} and {@link #cache} are concurrently safe.
118    *  <li>Checks for a cached item through getTotalCounts() are not synchronized.
119    *      Therefore, the case that a needed TFC is in the cache is very fast:
120    *      it does not wait for the computation of other TFCs.
121    *  <li>computeAndCache() is synchronized, and, has a (double) check of the required
122    *       TFC, to avoid computing the same TFC twice. 
123    *  <li>A race condition in this method (markRecentlyUsed) might result in two copies 
124    *      of the same 'key' in lruKeys, but this is handled by the loop in trimCache(), 
125    *      where an attempt to remove the same key twice is a no-op.
126    * </ol>
127    */
128   private void markRecentlyUsed(TFCKey key) {
129     lruKeys.remove(key);  
130     lruKeys.add(key);
131   }
132
133   private synchronized void trimCache() {
134     // loop until cache is of desired  size.
135     while (cache.size()>maxCacheSize ) { 
136       TFCKey key = lruKeys.poll();
137       if (key==null) { //defensive
138         // it is defensive since lruKeys presumably covers the cache keys 
139         key = cache.keys().nextElement(); 
140       }
141       // remove this element. Note that an attempt to remove with the same key again is a no-op,
142       // which gracefully handles the possible race in markRecentlyUsed(). 
143       cache.remove(key);
144     }
145   }
146   
147   /**
148    * compute TFC and cache it, after verifying it was not just added - for this
149    * matter this method is synchronized, which is not too bad, because there is
150    * lots of work done in the computations.
151    */
152   private synchronized TotalFacetCounts computeAndCache(TFCKey key, CategoryListCache clCache) throws IOException {
153     TotalFacetCounts tfc = cache.get(key); 
154     if (tfc == null) {
155       tfc = TotalFacetCounts.compute(key.indexReader, key.taxonomy, key.facetIndexingParams, clCache);
156       lruKeys.add(key);
157       cache.put(key,tfc);
158       trimCache();
159     }
160     return tfc;
161   }
162
163   /**
164    * Load {@link TotalFacetCounts} matching input parameters from the provided outputFile 
165    * and add them into the cache for the provided indexReader, taxonomy, and facetIndexingParams.
166    * If a {@link TotalFacetCounts} for these parameters already exists in the cache, it will be
167    * replaced by the loaded one.
168    * @param inputFile file from which to read the data 
169    * @param indexReader the documents index
170    * @param taxonomy the taxonomy index
171    * @param facetIndexingParams the facet indexing parameters
172    * @throws IOException on error
173    * @see #store(File, IndexReader, TaxonomyReader, FacetIndexingParams, CategoryListCache)
174    */
175   public synchronized void load(File inputFile, IndexReader indexReader, TaxonomyReader taxonomy,
176       FacetIndexingParams facetIndexingParams) throws IOException {
177     if (!inputFile.isFile() || !inputFile.exists() || !inputFile.canRead()) {
178       throw new IllegalArgumentException("Exepecting an existing readable file: "+inputFile);
179     }
180     TFCKey key = new TFCKey(indexReader, taxonomy, facetIndexingParams);
181     TotalFacetCounts tfc = TotalFacetCounts.loadFromFile(inputFile, taxonomy, facetIndexingParams);
182     cache.put(key,tfc);
183     trimCache();
184     markRecentlyUsed(key);
185   }
186   
187   /**
188    * Store the {@link TotalFacetCounts} matching input parameters into the provided outputFile,
189    * making them available for a later call to {@link #load(File, IndexReader, TaxonomyReader, FacetIndexingParams)}.
190    * If these {@link TotalFacetCounts} are available in the cache, they are used. But if they are
191    * not in the cache, this call will first compute them (which will also add them to the cache). 
192    * @param outputFile file to store in.
193    * @param indexReader the documents index
194    * @param taxonomy the taxonomy index
195    * @param facetIndexingParams the facet indexing parameters
196    * @param clCache category list cache for faster computation, can be null
197    * @throws IOException on error
198    * @see #load(File, IndexReader, TaxonomyReader, FacetIndexingParams)
199    * @see #getTotalCounts(IndexReader, TaxonomyReader, FacetIndexingParams, CategoryListCache)
200    */
201   public void store(File outputFile, IndexReader indexReader, TaxonomyReader taxonomy,
202       FacetIndexingParams facetIndexingParams, CategoryListCache clCache) throws IOException {
203     File parentFile = outputFile.getParentFile();
204     if (
205         ( outputFile.exists() && (!outputFile.isFile()      || !outputFile.canWrite())) ||
206         (!outputFile.exists() && (!parentFile.isDirectory() || !parentFile.canWrite()))
207       ) {
208       throw new IllegalArgumentException("Exepecting a writable file: "+outputFile);
209     }
210     TotalFacetCounts tfc = getTotalCounts(indexReader, taxonomy, facetIndexingParams, clCache);
211     TotalFacetCounts.storeToFile(outputFile, tfc);  
212   }
213   
214   private static class TFCKey {
215     final IndexReader indexReader;
216     final TaxonomyReader taxonomy;
217     private final Iterable<CategoryListParams> clps;
218     private final int hashCode;
219     private final int nDels; // needed when a reader used for faceted search was just used for deletion. 
220     final FacetIndexingParams facetIndexingParams;
221     
222     public TFCKey(IndexReader indexReader, TaxonomyReader taxonomy,
223         FacetIndexingParams facetIndexingParams) {
224       this.indexReader = indexReader;
225       this.taxonomy = taxonomy;
226       this.facetIndexingParams = facetIndexingParams;
227       this.clps = facetIndexingParams.getAllCategoryListParams();
228       this.nDels = indexReader.numDeletedDocs();
229       hashCode = indexReader.hashCode() ^ taxonomy.hashCode();
230     }
231     
232     @Override
233     public int hashCode() {
234       return hashCode;
235     }
236     
237     @Override
238     public boolean equals(Object other) {
239       TFCKey o = (TFCKey) other; 
240       if (indexReader != o.indexReader || taxonomy != o.taxonomy || nDels != o.nDels) {
241         return false;
242       }
243       Iterator<CategoryListParams> it1 = clps.iterator();
244       Iterator<CategoryListParams> it2 = o.clps.iterator();
245       while (it1.hasNext() && it2.hasNext()) {
246         if (!it1.next().equals(it2.next())) {
247           return false;
248         }
249       }
250       return it1.hasNext() == it2.hasNext();
251     }
252   }
253
254   /**
255    * Clear the cache.
256    */
257   public synchronized void clear() {
258     cache.clear();
259     lruKeys.clear();
260   }
261   
262   /**
263    * @return the maximal cache size
264    */
265   public int getCacheSize() {
266     return maxCacheSize;
267   }
268
269   /**
270    * Set the number of TotalFacetCounts arrays that will remain in memory cache.
271    * <p>
272    * If new size is smaller than current size, the cache is appropriately trimmed.
273    * <p>
274    * Minimal size is 1, so passing zero or negative size would result in size of 1.
275    * @param size new size to set
276    */
277   public void setCacheSize(int size) {
278     if (size < 1) size = 1;
279     int origSize = maxCacheSize;
280     maxCacheSize = size;
281     if (maxCacheSize < origSize) { // need to trim only if the cache was reduced
282       trimCache();
283     }
284   }
285 }