pylucene 3.5.0-3
[pylucene.git] / lucene-java-3.5.0 / lucene / src / java / org / apache / lucene / store / MMapDirectory.java
1 package org.apache.lucene.store;
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.io.IOException;
21 import java.io.File;
22 import java.io.RandomAccessFile;
23 import java.nio.ByteBuffer;
24 import java.nio.BufferUnderflowException;
25 import java.nio.channels.ClosedChannelException; // javadoc
26 import java.nio.channels.FileChannel;
27 import java.nio.channels.FileChannel.MapMode;
28
29 import java.security.AccessController;
30 import java.security.PrivilegedExceptionAction;
31 import java.security.PrivilegedActionException;
32 import java.lang.reflect.Method;
33
34 import org.apache.lucene.util.Constants;
35
36 /** File-based {@link Directory} implementation that uses
37  *  mmap for reading, and {@link
38  *  FSDirectory.FSIndexOutput} for writing.
39  *
40  * <p><b>NOTE</b>: memory mapping uses up a portion of the
41  * virtual memory address space in your process equal to the
42  * size of the file being mapped.  Before using this class,
43  * be sure your have plenty of virtual address space, e.g. by
44  * using a 64 bit JRE, or a 32 bit JRE with indexes that are
45  * guaranteed to fit within the address space.
46  * On 32 bit platforms also consult {@link #setMaxChunkSize}
47  * if you have problems with mmap failing because of fragmented
48  * address space. If you get an OutOfMemoryException, it is recommended
49  * to reduce the chunk size, until it works.
50  *
51  * <p>Due to <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4724038">
52  * this bug</a> in Sun's JRE, MMapDirectory's {@link IndexInput#close}
53  * is unable to close the underlying OS file handle.  Only when GC
54  * finally collects the underlying objects, which could be quite
55  * some time later, will the file handle be closed.
56  *
57  * <p>This will consume additional transient disk usage: on Windows,
58  * attempts to delete or overwrite the files will result in an
59  * exception; on other platforms, which typically have a &quot;delete on
60  * last close&quot; semantics, while such operations will succeed, the bytes
61  * are still consuming space on disk.  For many applications this
62  * limitation is not a problem (e.g. if you have plenty of disk space,
63  * and you don't rely on overwriting files on Windows) but it's still
64  * an important limitation to be aware of.
65  *
66  * <p>This class supplies the workaround mentioned in the bug report
67  * (see {@link #setUseUnmap}), which may fail on
68  * non-Sun JVMs. It forcefully unmaps the buffer on close by using
69  * an undocumented internal cleanup functionality.
70  * {@link #UNMAP_SUPPORTED} is <code>true</code>, if the workaround
71  * can be enabled (with no guarantees).
72  * <p>
73  * <b>NOTE:</b> Accessing this class either directly or
74  * indirectly from a thread while it's interrupted can close the
75  * underlying channel immediately if at the same time the thread is
76  * blocked on IO. The channel will remain closed and subsequent access
77  * to {@link MMapDirectory} will throw a {@link ClosedChannelException}. 
78  * </p>
79  */
80 public class MMapDirectory extends FSDirectory {
81   private boolean useUnmapHack = UNMAP_SUPPORTED;
82   public static final int DEFAULT_MAX_BUFF = Constants.JRE_IS_64BIT ? (1 << 30) : (1 << 28);
83   private int chunkSizePower;
84
85   /** Create a new MMapDirectory for the named location.
86    *
87    * @param path the path of the directory
88    * @param lockFactory the lock factory to use, or null for the default
89    * ({@link NativeFSLockFactory});
90    * @throws IOException
91    */
92   public MMapDirectory(File path, LockFactory lockFactory) throws IOException {
93     super(path, lockFactory);
94     setMaxChunkSize(DEFAULT_MAX_BUFF);
95   }
96
97   /** Create a new MMapDirectory for the named location and {@link NativeFSLockFactory}.
98    *
99    * @param path the path of the directory
100    * @throws IOException
101    */
102   public MMapDirectory(File path) throws IOException {
103     super(path, null);
104     setMaxChunkSize(DEFAULT_MAX_BUFF);
105   }
106
107   /**
108    * <code>true</code>, if this platform supports unmapping mmapped files.
109    */
110   public static final boolean UNMAP_SUPPORTED;
111   static {
112     boolean v;
113     try {
114       Class.forName("sun.misc.Cleaner");
115       Class.forName("java.nio.DirectByteBuffer")
116         .getMethod("cleaner");
117       v = true;
118     } catch (Exception e) {
119       v = false;
120     }
121     UNMAP_SUPPORTED = v;
122   }
123   
124   /**
125    * This method enables the workaround for unmapping the buffers
126    * from address space after closing {@link IndexInput}, that is
127    * mentioned in the bug report. This hack may fail on non-Sun JVMs.
128    * It forcefully unmaps the buffer on close by using
129    * an undocumented internal cleanup functionality.
130    * <p><b>NOTE:</b> Enabling this is completely unsupported
131    * by Java and may lead to JVM crashes if <code>IndexInput</code>
132    * is closed while another thread is still accessing it (SIGSEGV).
133    * @throws IllegalArgumentException if {@link #UNMAP_SUPPORTED}
134    * is <code>false</code> and the workaround cannot be enabled.
135    */
136   public void setUseUnmap(final boolean useUnmapHack) {
137     if (useUnmapHack && !UNMAP_SUPPORTED)
138       throw new IllegalArgumentException("Unmap hack not supported on this platform!");
139     this.useUnmapHack=useUnmapHack;
140   }
141   
142   /**
143    * Returns <code>true</code>, if the unmap workaround is enabled.
144    * @see #setUseUnmap
145    */
146   public boolean getUseUnmap() {
147     return useUnmapHack;
148   }
149   
150   /**
151    * Try to unmap the buffer, this method silently fails if no support
152    * for that in the JVM. On Windows, this leads to the fact,
153    * that mmapped files cannot be modified or deleted.
154    */
155   final void cleanMapping(final ByteBuffer buffer) throws IOException {
156     if (useUnmapHack) {
157       try {
158         AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
159           public Object run() throws Exception {
160             final Method getCleanerMethod = buffer.getClass()
161               .getMethod("cleaner");
162             getCleanerMethod.setAccessible(true);
163             final Object cleaner = getCleanerMethod.invoke(buffer);
164             if (cleaner != null) {
165               cleaner.getClass().getMethod("clean")
166                 .invoke(cleaner);
167             }
168             return null;
169           }
170         });
171       } catch (PrivilegedActionException e) {
172         final IOException ioe = new IOException("unable to unmap the mapped buffer");
173         ioe.initCause(e.getCause());
174         throw ioe;
175       }
176     }
177   }
178   
179   /**
180    * Sets the maximum chunk size (default is {@link Integer#MAX_VALUE} for
181    * 64 bit JVMs and 256 MiBytes for 32 bit JVMs) used for memory mapping.
182    * Especially on 32 bit platform, the address space can be very fragmented,
183    * so large index files cannot be mapped.
184    * Using a lower chunk size makes the directory implementation a little
185    * bit slower (as the correct chunk may be resolved on lots of seeks)
186    * but the chance is higher that mmap does not fail. On 64 bit
187    * Java platforms, this parameter should always be {@code 1 << 30},
188    * as the address space is big enough.
189    * <b>Please note:</b> This method always rounds down the chunk size
190    * to a power of 2.
191    */
192   public final void setMaxChunkSize(final int maxChunkSize) {
193     if (maxChunkSize <= 0)
194       throw new IllegalArgumentException("Maximum chunk size for mmap must be >0");
195     //System.out.println("Requested chunk size: "+maxChunkSize);
196     this.chunkSizePower = 31 - Integer.numberOfLeadingZeros(maxChunkSize);
197     assert this.chunkSizePower >= 0 && this.chunkSizePower <= 30;
198     //System.out.println("Got chunk size: "+getMaxChunkSize());
199   }
200   
201   /**
202    * Returns the current mmap chunk size.
203    * @see #setMaxChunkSize
204    */
205   public final int getMaxChunkSize() {
206     return 1 << chunkSizePower;
207   }
208
209   /** Creates an IndexInput for the file with the given name. */
210   @Override
211   public IndexInput openInput(String name, int bufferSize) throws IOException {
212     ensureOpen();
213     File f = new File(getDirectory(), name);
214     RandomAccessFile raf = new RandomAccessFile(f, "r");
215     try {
216       return new MMapIndexInput("MMapIndexInput(path=\"" + f + "\")", raf, chunkSizePower);
217     } finally {
218       raf.close();
219     }
220   }
221
222   // Because Java's ByteBuffer uses an int to address the
223   // values, it's necessary to access a file >
224   // Integer.MAX_VALUE in size using multiple byte buffers.
225   private final class MMapIndexInput extends IndexInput {
226   
227     private ByteBuffer[] buffers;
228   
229     private final long length, chunkSizeMask, chunkSize;
230     private final int chunkSizePower;
231   
232     private int curBufIndex;
233   
234     private ByteBuffer curBuf; // redundant for speed: buffers[curBufIndex]
235   
236     private boolean isClone = false;
237     
238     MMapIndexInput(String resourceDescription, RandomAccessFile raf, int chunkSizePower) throws IOException {
239       super(resourceDescription);
240       this.length = raf.length();
241       this.chunkSizePower = chunkSizePower;
242       this.chunkSize = 1L << chunkSizePower;
243       this.chunkSizeMask = chunkSize - 1L;
244       
245       if (chunkSizePower < 0 || chunkSizePower > 30)
246         throw new IllegalArgumentException("Invalid chunkSizePower used for ByteBuffer size: " + chunkSizePower);
247       
248       if ((length >>> chunkSizePower) >= Integer.MAX_VALUE)
249         throw new IllegalArgumentException("RandomAccessFile too big for chunk size: " + raf.toString());
250       
251       // we always allocate one more buffer, the last one may be a 0 byte one
252       final int nrBuffers = (int) (length >>> chunkSizePower) + 1;
253       
254       //System.out.println("length="+length+", chunkSizePower=" + chunkSizePower + ", chunkSizeMask=" + chunkSizeMask + ", nrBuffers=" + nrBuffers);
255       
256       this.buffers = new ByteBuffer[nrBuffers];
257       
258       long bufferStart = 0L;
259       FileChannel rafc = raf.getChannel();
260       for (int bufNr = 0; bufNr < nrBuffers; bufNr++) { 
261         int bufSize = (int) ( (length > (bufferStart + chunkSize))
262           ? chunkSize
263           : (length - bufferStart)
264         );
265         this.buffers[bufNr] = rafc.map(MapMode.READ_ONLY, bufferStart, bufSize);
266         bufferStart += bufSize;
267       }
268       seek(0L);
269     }
270   
271     @Override
272     public byte readByte() throws IOException {
273       try {
274         return curBuf.get();
275       } catch (BufferUnderflowException e) {
276         do {
277           curBufIndex++;
278           if (curBufIndex >= buffers.length) {
279             throw new IOException("read past EOF: " + this);
280           }
281           curBuf = buffers[curBufIndex];
282           curBuf.position(0);
283         } while (!curBuf.hasRemaining());
284         return curBuf.get();
285       }
286     }
287   
288     @Override
289     public void readBytes(byte[] b, int offset, int len) throws IOException {
290       try {
291         curBuf.get(b, offset, len);
292       } catch (BufferUnderflowException e) {
293         int curAvail = curBuf.remaining();
294         while (len > curAvail) {
295           curBuf.get(b, offset, curAvail);
296           len -= curAvail;
297           offset += curAvail;
298           curBufIndex++;
299           if (curBufIndex >= buffers.length) {
300             throw new IOException("read past EOF: " + this);
301           }
302           curBuf = buffers[curBufIndex];
303           curBuf.position(0);
304           curAvail = curBuf.remaining();
305         }
306         curBuf.get(b, offset, len);
307       }
308     }
309
310     @Override
311     public int readInt() throws IOException {
312       try {
313         return curBuf.getInt();
314       } catch (BufferUnderflowException e) {
315         return super.readInt();
316       }
317     }
318
319     @Override
320     public long readLong() throws IOException {
321       try {
322         return curBuf.getLong();
323       } catch (BufferUnderflowException e) {
324         return super.readLong();
325       }
326     }
327     
328     @Override
329     public long getFilePointer() {
330       return (((long) curBufIndex) << chunkSizePower) + curBuf.position();
331     }
332   
333     @Override
334     public void seek(long pos) throws IOException {
335       // we use >> here to preserve negative, so we will catch AIOOBE:
336       final int bi = (int) (pos >> chunkSizePower);
337       try {
338         final ByteBuffer b = buffers[bi];
339         b.position((int) (pos & chunkSizeMask));
340         // write values, on exception all is unchanged
341         this.curBufIndex = bi;
342         this.curBuf = b;
343       } catch (ArrayIndexOutOfBoundsException aioobe) {
344         if (pos < 0L) {
345           throw new IllegalArgumentException("Seeking to negative position: " + this);
346         }
347         throw new IOException("seek past EOF");
348       } catch (IllegalArgumentException iae) {
349         if (pos < 0L) {
350           throw new IllegalArgumentException("Seeking to negative position: " + this);
351         }
352         throw new IOException("seek past EOF: " + this);
353       }
354     }
355   
356     @Override
357     public long length() {
358       return length;
359     }
360   
361     @Override
362     public Object clone() {
363       if (buffers == null) {
364         throw new AlreadyClosedException("MMapIndexInput already closed: " + this);
365       }
366       final MMapIndexInput clone = (MMapIndexInput)super.clone();
367       clone.isClone = true;
368       clone.buffers = new ByteBuffer[buffers.length];
369       // Since most clones will use only one buffer, duplicate() could also be
370       // done lazy in clones, e.g. when adapting curBuf.
371       for (int bufNr = 0; bufNr < buffers.length; bufNr++) {
372         clone.buffers[bufNr] = buffers[bufNr].duplicate();
373       }
374       try {
375         clone.seek(getFilePointer());
376       } catch(IOException ioe) {
377         throw new RuntimeException("Should never happen: " + this, ioe);
378       }
379       return clone;
380     }
381   
382     @Override
383     public void close() throws IOException {
384       try {
385         if (isClone || buffers == null) return;
386         for (int bufNr = 0; bufNr < buffers.length; bufNr++) {
387           // unmap the buffer (if enabled) and at least unset it for GC
388           try {
389             cleanMapping(buffers[bufNr]);
390           } finally {
391             buffers[bufNr] = null;
392           }
393         }
394       } finally {
395         buffers = null;
396       }
397     }
398   }
399 }