pylucene 3.5.0-3
[pylucene.git] / lucene-java-3.5.0 / lucene / src / java / org / apache / lucene / index / SnapshotDeletionPolicy.java
1 package org.apache.lucene.index;
2
3 /**
4  * Licensed to the Apache Software Foundation (ASF) under one or more
5  * contributor license agreements.  See the NOTICE file distributed with
6  * this work for additional information regarding copyright ownership.
7  * The ASF licenses this file to You under the Apache License, Version 2.0
8  * (the "License"); you may not use this file except in compliance with
9  * the License.  You may obtain a copy of the License at
10  *
11  *     http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.ArrayList;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.Map.Entry;
28 import java.io.IOException;
29
30 import org.apache.lucene.store.Directory;
31
32 /**
33  * An {@link IndexDeletionPolicy} that wraps around any other
34  * {@link IndexDeletionPolicy} and adds the ability to hold and later release
35  * snapshots of an index. While a snapshot is held, the {@link IndexWriter} will
36  * not remove any files associated with it even if the index is otherwise being
37  * actively, arbitrarily changed. Because we wrap another arbitrary
38  * {@link IndexDeletionPolicy}, this gives you the freedom to continue using
39  * whatever {@link IndexDeletionPolicy} you would normally want to use with your
40  * index.
41  * 
42  * <p>
43  * This class maintains all snapshots in-memory, and so the information is not
44  * persisted and not protected against system failures. If persistency is
45  * important, you can use {@link PersistentSnapshotDeletionPolicy} (or your own
46  * extension) and when creating a new instance of this deletion policy, pass the
47  * persistent snapshots information to
48  * {@link #SnapshotDeletionPolicy(IndexDeletionPolicy, Map)}.
49  * 
50  * @lucene.experimental
51  */
52 public class SnapshotDeletionPolicy implements IndexDeletionPolicy {
53
54   /** Holds a Snapshot's information. */
55   private static class SnapshotInfo {
56     String id;
57     String segmentsFileName;
58     IndexCommit commit;
59     
60     public SnapshotInfo(String id, String segmentsFileName, IndexCommit commit) {
61       this.id = id;
62       this.segmentsFileName = segmentsFileName;
63       this.commit = commit;
64     }
65     
66     @Override
67     public String toString() {
68       return id + " : " + segmentsFileName;
69     }
70   }
71   
72   protected class SnapshotCommitPoint extends IndexCommit {
73     protected IndexCommit cp;
74
75     protected SnapshotCommitPoint(IndexCommit cp) {
76       this.cp = cp;
77     }
78
79     @Override
80     public String toString() {
81       return "SnapshotDeletionPolicy.SnapshotCommitPoint(" + cp + ")";
82     }
83
84     /**
85      * Returns true if this segment can be deleted. The default implementation
86      * returns false if this segment is currently held as snapshot.
87      */
88     protected boolean shouldDelete(String segmentsFileName) {
89       return !segmentsFileToIDs.containsKey(segmentsFileName);
90     }
91
92     @Override
93     public void delete() {
94       synchronized (SnapshotDeletionPolicy.this) {
95         // Suppress the delete request if this commit point is
96         // currently snapshotted.
97         if (shouldDelete(getSegmentsFileName())) {
98           cp.delete();
99         }
100       }
101     }
102
103     @Override
104     public Directory getDirectory() {
105       return cp.getDirectory();
106     }
107
108     @Override
109     public Collection<String> getFileNames() throws IOException {
110       return cp.getFileNames();
111     }
112
113     @Override
114     public long getGeneration() {
115       return cp.getGeneration();
116     }
117
118     @Override
119     public String getSegmentsFileName() {
120       return cp.getSegmentsFileName();
121     }
122
123     @Override
124     public Map<String, String> getUserData() throws IOException {
125       return cp.getUserData();
126     }
127
128     @Override
129     public long getVersion() {
130       return cp.getVersion();
131     }
132
133     @Override
134     public boolean isDeleted() {
135       return cp.isDeleted();
136     }
137
138     @Override
139     public int getSegmentCount() {
140       return cp.getSegmentCount();
141     }
142   }
143
144   /** Snapshots info */
145   private Map<String, SnapshotInfo> idToSnapshot = new HashMap<String, SnapshotInfo>();
146
147   // multiple IDs could point to the same commit point (segments file name)
148   private Map<String, Set<String>> segmentsFileToIDs = new HashMap<String, Set<String>>();
149
150   private IndexDeletionPolicy primary;
151   protected IndexCommit lastCommit;
152
153   public SnapshotDeletionPolicy(IndexDeletionPolicy primary) {
154     this.primary = primary;
155   }
156
157   /**
158    * {@link SnapshotDeletionPolicy} wraps another {@link IndexDeletionPolicy} to
159    * enable flexible snapshotting.
160    * 
161    * @param primary
162    *          the {@link IndexDeletionPolicy} that is used on non-snapshotted
163    *          commits. Snapshotted commits, are not deleted until explicitly
164    *          released via {@link #release(String)}
165    * @param snapshotsInfo
166    *          A mapping of snapshot ID to the segments filename that is being
167    *          snapshotted. The expected input would be the output of
168    *          {@link #getSnapshots()}. A null value signals that there are no
169    *          initial snapshots to maintain.
170    */
171   public SnapshotDeletionPolicy(IndexDeletionPolicy primary,
172       Map<String, String> snapshotsInfo) {
173     this(primary);
174
175     if (snapshotsInfo != null) {
176       // Add the ID->segmentIDs here - the actual IndexCommits will be
177       // reconciled on the call to onInit()
178       for (Entry<String, String> e : snapshotsInfo.entrySet()) {
179         registerSnapshotInfo(e.getKey(), e.getValue(), null);
180       }
181     }
182   }
183
184   /**
185    * Checks if the given id is already used by another snapshot, and throws
186    * {@link IllegalStateException} if it is.
187    */
188   protected void checkSnapshotted(String id) {
189     if (isSnapshotted(id)) {
190       throw new IllegalStateException("Snapshot ID " + id
191           + " is already used - must be unique");
192     }
193   }
194
195   /** Registers the given snapshot information. */
196   protected void registerSnapshotInfo(String id, String segment, IndexCommit commit) {
197     idToSnapshot.put(id, new SnapshotInfo(id, segment, commit));
198     Set<String> ids = segmentsFileToIDs.get(segment);
199     if (ids == null) {
200       ids = new HashSet<String>();
201       segmentsFileToIDs.put(segment, ids);
202     }
203     ids.add(id);
204   }
205
206   protected List<IndexCommit> wrapCommits(List<? extends IndexCommit> commits) {
207     List<IndexCommit> wrappedCommits = new ArrayList<IndexCommit>(commits.size());
208     for (IndexCommit ic : commits) {
209       wrappedCommits.add(new SnapshotCommitPoint(ic));
210     }
211     return wrappedCommits;
212   }
213
214   /**
215    * Get a snapshotted IndexCommit by ID. The IndexCommit can then be used to
216    * open an IndexReader on a specific commit point, or rollback the index by
217    * opening an IndexWriter with the IndexCommit specified in its
218    * {@link IndexWriterConfig}.
219    * 
220    * @param id
221    *          a unique identifier of the commit that was snapshotted.
222    * @throws IllegalStateException
223    *           if no snapshot exists by the specified ID.
224    * @return The {@link IndexCommit} for this particular snapshot.
225    */
226   public synchronized IndexCommit getSnapshot(String id) {
227     SnapshotInfo snapshotInfo = idToSnapshot.get(id);
228     if (snapshotInfo == null) {
229       throw new IllegalStateException("No snapshot exists by ID: " + id);
230     }
231     return snapshotInfo.commit;
232   }
233
234   /**
235    * Get all the snapshots in a map of snapshot IDs to the segments they
236    * 'cover.' This can be passed to
237    * {@link #SnapshotDeletionPolicy(IndexDeletionPolicy, Map)} in order to
238    * initialize snapshots at construction.
239    */
240   public synchronized Map<String, String> getSnapshots() {
241     Map<String, String> snapshots = new HashMap<String, String>();
242     for (Entry<String, SnapshotInfo> e : idToSnapshot.entrySet()) {
243       snapshots.put(e.getKey(), e.getValue().segmentsFileName);
244     }
245     return snapshots;
246   }
247
248   /**
249    * Returns true if the given ID is already used by a snapshot. You can call
250    * this method before {@link #snapshot(String)} if you are not sure whether
251    * the ID is already used or not.
252    */
253   public boolean isSnapshotted(String id) {
254     return idToSnapshot.containsKey(id);
255   }
256
257   public synchronized void onCommit(List<? extends IndexCommit> commits)
258       throws IOException {
259     primary.onCommit(wrapCommits(commits));
260     lastCommit = commits.get(commits.size() - 1);
261   }
262
263   public synchronized void onInit(List<? extends IndexCommit> commits)
264       throws IOException {
265     primary.onInit(wrapCommits(commits));
266     lastCommit = commits.get(commits.size() - 1);
267
268     /*
269      * Assign snapshotted IndexCommits to their correct snapshot IDs as
270      * specified in the constructor.
271      */
272     for (IndexCommit commit : commits) {
273       Set<String> ids = segmentsFileToIDs.get(commit.getSegmentsFileName());
274       if (ids != null) {
275         for (String id : ids) {
276           idToSnapshot.get(id).commit = commit;
277         }
278       }
279     }
280
281     /*
282      * Second, see if there are any instances where a snapshot ID was specified
283      * in the constructor but an IndexCommit doesn't exist. In this case, the ID
284      * should be removed.
285      * 
286      * Note: This code is protective for extreme cases where IDs point to
287      * non-existent segments. As the constructor should have received its
288      * information via a call to getSnapshots(), the data should be well-formed.
289      */
290     // Find lost snapshots
291     ArrayList<String> idsToRemove = null;
292     for (Entry<String, SnapshotInfo> e : idToSnapshot.entrySet()) {
293       if (e.getValue().commit == null) {
294         if (idsToRemove == null) {
295           idsToRemove = new ArrayList<String>();
296         }
297         idsToRemove.add(e.getKey());
298       }
299     }
300     // Finally, remove those 'lost' snapshots.
301     if (idsToRemove != null) {
302       for (String id : idsToRemove) {
303         SnapshotInfo info = idToSnapshot.remove(id);
304         segmentsFileToIDs.remove(info.segmentsFileName);
305       }
306     }
307   }
308
309   /**
310    * Release a snapshotted commit by ID.
311    * 
312    * @param id
313    *          a unique identifier of the commit that is un-snapshotted.
314    * @throws IllegalStateException
315    *           if no snapshot exists by this ID.
316    */
317   public synchronized void release(String id) throws IOException {
318     SnapshotInfo info = idToSnapshot.remove(id);
319     if (info == null) {
320       throw new IllegalStateException("Snapshot doesn't exist: " + id);
321     }
322     Set<String> ids = segmentsFileToIDs.get(info.segmentsFileName);
323     if (ids != null) {
324       ids.remove(id);
325       if (ids.size() == 0) {
326         segmentsFileToIDs.remove(info.segmentsFileName);
327       }
328     }
329   }
330
331   /**
332    * Snapshots the last commit. Once a commit is 'snapshotted,' it is protected
333    * from deletion (as long as this {@link IndexDeletionPolicy} is used). The
334    * commit can be removed by calling {@link #release(String)} using the same ID
335    * parameter followed by a call to {@link IndexWriter#deleteUnusedFiles()}.
336    * <p>
337    * <b>NOTE:</b> ID must be unique in the system. If the same ID is used twice,
338    * an {@link IllegalStateException} is thrown.
339    * <p>
340    * <b>NOTE:</b> while the snapshot is held, the files it references will not
341    * be deleted, which will consume additional disk space in your index. If you
342    * take a snapshot at a particularly bad time (say just before you call
343    * forceMerge) then in the worst case this could consume an extra 1X of your
344    * total index size, until you release the snapshot.
345    * 
346    * @param id
347    *          a unique identifier of the commit that is being snapshotted.
348    * @throws IllegalStateException
349    *           if either there is no 'last commit' to snapshot, or if the
350    *           parameter 'ID' refers to an already snapshotted commit.
351    * @return the {@link IndexCommit} that was snapshotted.
352    */
353   public synchronized IndexCommit snapshot(String id) throws IOException {
354     if (lastCommit == null) {
355       // no commit exists. Really shouldn't happen, but might be if SDP is
356       // accessed before onInit or onCommit were called.
357       throw new IllegalStateException("No index commit to snapshot");
358     }
359
360     // Can't use the same snapshot ID twice...
361     checkSnapshotted(id);
362
363     registerSnapshotInfo(id, lastCommit.getSegmentsFileName(), lastCommit);
364     return lastCommit;
365   }
366
367 }