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.nio.channels.FileChannel;
21  import java.nio.channels.FileLock;
22  import java.io.File;
23  import java.io.RandomAccessFile;
24  import java.io.IOException;
25  import java.util.HashSet;
26  import java.util.Random;
27  
28  /**
29   * <p>Implements {@link LockFactory} using native OS file
30   * locks.  Note that because this LockFactory relies on
31   * java.nio.* APIs for locking, any problems with those APIs
32   * will cause locking to fail.  Specifically, on certain NFS
33   * environments the java.nio.* locks will fail (the lock can
34   * incorrectly be double acquired) whereas {@link
35   * SimpleFSLockFactory} worked perfectly in those same
36   * environments.  For NFS based access to an index, it's
37   * recommended that you try {@link SimpleFSLockFactory}
38   * first and work around the one limitation that a lock file
39   * could be left when the JVM exits abnormally.</p>
40   *
41   * <p>The primary benefit of {@link NativeFSLockFactory} is
42   * that lock files will be properly removed (by the OS) if
43   * the JVM has an abnormal exit.</p>
44   * 
45   * <p>Note that, unlike {@link SimpleFSLockFactory}, the existence of
46   * leftover lock files in the filesystem on exiting the JVM
47   * is fine because the OS will free the locks held against
48   * these files even though the files still remain.</p>
49   *
50   * <p>If you suspect that this or any other LockFactory is
51   * not working properly in your environment, you can easily
52   * test it by using {@link VerifyingLockFactory}, {@link
53   * LockVerifyServer} and {@link LockStressTest}.</p>
54   *
55   * @see LockFactory
56   */
57  
58  public class NativeFSLockFactory extends LockFactory {
59  
60    /**
61     * Directory specified by <code>org.apache.lucene.lockDir</code>
62     * system property.  If that is not set, then <code>java.io.tmpdir</code>
63     * system property is used.
64     */
65  
66    private File lockDir;
67  
68    // Simple test to verify locking system is "working".  On
69    // NFS, if it's misconfigured, you can hit long (35
70    // second) timeouts which cause Lock.obtain to take far
71    // too long (it assumes the obtain() call takes zero
72    // time).  Since it's a configuration problem, we test up
73    // front once on creating the LockFactory:
74    private void acquireTestLock() throws IOException {
75      String randomLockName = "lucene-" + Long.toString(new Random().nextInt(), Character.MAX_RADIX) + "-test.lock";
76      
77      Lock l = makeLock(randomLockName);
78      try {
79        l.obtain();
80      } catch (IOException e) {
81        IOException e2 = new IOException("Failed to acquire random test lock; please verify filesystem for lock directory '" + lockDir + "' supports locking");
82        e2.initCause(e);
83        throw e2;
84      }
85  
86      l.release();
87    }
88  
89    /**
90     * Create a NativeFSLockFactory instance, with null (unset)
91     * lock directory.  This is package-private and is only
92     * used by FSDirectory when creating this LockFactory via
93     * the System property
94     * org.apache.lucene.store.FSDirectoryLockFactoryClass.
95     */
96    NativeFSLockFactory() throws IOException {
97      this((File) null);
98    }
99  
100   /**
101    * Create a NativeFSLockFactory instance, storing lock
102    * files into the specified lockDirName:
103    *
104    * @param lockDirName where lock files are created.
105    */
106   public NativeFSLockFactory(String lockDirName) throws IOException {
107     this(new File(lockDirName));
108   }
109 
110   /**
111    * Create a NativeFSLockFactory instance, storing lock
112    * files into the specified lockDir:
113    * 
114    * @param lockDir where lock files are created.
115    */
116   public NativeFSLockFactory(File lockDir) throws IOException {
117     setLockDir(lockDir);
118   }
119 
120   /**
121    * Set the lock directory.  This is package-private and is
122    * only used externally by FSDirectory when creating this
123    * LockFactory via the System property
124    * org.apache.lucene.store.FSDirectoryLockFactoryClass.
125    */
126   void setLockDir(File lockDir) throws IOException {
127     this.lockDir = lockDir;
128     if (lockDir != null) {
129       // Ensure that lockDir exists and is a directory.
130       if (!lockDir.exists()) {
131         if (!lockDir.mkdirs())
132           throw new IOException("Cannot create directory: " +
133                                 lockDir.getAbsolutePath());
134       } else if (!lockDir.isDirectory()) {
135         throw new IOException("Found regular file where directory expected: " + 
136                               lockDir.getAbsolutePath());
137       }
138 
139       acquireTestLock();
140     }
141   }
142 
143   public synchronized Lock makeLock(String lockName) {
144     if (lockPrefix != null)
145       lockName = lockPrefix + "-n-" + lockName;
146     return new NativeFSLock(lockDir, lockName);
147   }
148 
149   public void clearLock(String lockName) throws IOException {
150     // Note that this isn't strictly required anymore
151     // because the existence of these files does not mean
152     // they are locked, but, still do this in case people
153     // really want to see the files go away:
154     if (lockDir.exists()) {
155       if (lockPrefix != null) {
156         lockName = lockPrefix + "-n-" + lockName;
157       }
158       File lockFile = new File(lockDir, lockName);
159       if (lockFile.exists() && !lockFile.delete()) {
160         throw new IOException("Cannot delete " + lockFile);
161       }
162     }
163   }
164 };
165 
166 class NativeFSLock extends Lock {
167 
168   private RandomAccessFile f;
169   private FileChannel channel;
170   private FileLock lock;
171   private File path;
172   private File lockDir;
173 
174   /*
175    * The javadocs for FileChannel state that you should have
176    * a single instance of a FileChannel (per JVM) for all
177    * locking against a given file.  To ensure this, we have
178    * a single (static) HashSet that contains the file paths
179    * of all currently locked locks.  This protects against
180    * possible cases where different Directory instances in
181    * one JVM (each with their own NativeFSLockFactory
182    * instance) have set the same lock dir and lock prefix.
183    */
184   private static HashSet LOCK_HELD = new HashSet();
185 
186   public NativeFSLock(File lockDir, String lockFileName) {
187     this.lockDir = lockDir;
188     path = new File(lockDir, lockFileName);
189   }
190 
191   public synchronized boolean obtain() throws IOException {
192 
193     if (isLocked()) {
194       // Our instance is already locked:
195       return false;
196     }
197 
198     // Ensure that lockDir exists and is a directory.
199     if (!lockDir.exists()) {
200       if (!lockDir.mkdirs())
201         throw new IOException("Cannot create directory: " +
202                               lockDir.getAbsolutePath());
203     } else if (!lockDir.isDirectory()) {
204       throw new IOException("Found regular file where directory expected: " + 
205                             lockDir.getAbsolutePath());
206     }
207 
208     String canonicalPath = path.getCanonicalPath();
209 
210     boolean markedHeld = false;
211 
212     try {
213 
214       // Make sure nobody else in-process has this lock held
215       // already, and, mark it held if not:
216 
217       synchronized(LOCK_HELD) {
218         if (LOCK_HELD.contains(canonicalPath)) {
219           // Someone else in this JVM already has the lock:
220           return false;
221         } else {
222           // This "reserves" the fact that we are the one
223           // thread trying to obtain this lock, so we own
224           // the only instance of a channel against this
225           // file:
226           LOCK_HELD.add(canonicalPath);
227           markedHeld = true;
228         }
229       }
230 
231       try {
232         f = new RandomAccessFile(path, "rw");
233       } catch (IOException e) {
234         // On Windows, we can get intermittant "Access
235         // Denied" here.  So, we treat this as failure to
236         // acquire the lock, but, store the reason in case
237         // there is in fact a real error case.
238         failureReason = e;
239         f = null;
240       }
241 
242       if (f != null) {
243         try {
244           channel = f.getChannel();
245           try {
246             lock = channel.tryLock();
247           } catch (IOException e) {
248             // At least on OS X, we will sometimes get an
249             // intermittant "Permission Denied" IOException,
250             // which seems to simply mean "you failed to get
251             // the lock".  But other IOExceptions could be
252             // "permanent" (eg, locking is not supported via
253             // the filesystem).  So, we record the failure
254             // reason here; the timeout obtain (usually the
255             // one calling us) will use this as "root cause"
256             // if it fails to get the lock.
257             failureReason = e;
258           } finally {
259             if (lock == null) {
260               try {
261                 channel.close();
262               } finally {
263                 channel = null;
264               }
265             }
266           }
267         } finally {
268           if (channel == null) {
269             try {
270               f.close();
271             } finally {
272               f = null;
273             }
274           }
275         }
276       }
277 
278     } finally {
279       if (markedHeld && !isLocked()) {
280         synchronized(LOCK_HELD) {
281           if (LOCK_HELD.contains(canonicalPath)) {
282             LOCK_HELD.remove(canonicalPath);
283           }
284         }
285       }
286     }
287     return isLocked();
288   }
289 
290   public synchronized void release() throws IOException {
291     if (isLocked()) {
292       try {
293         lock.release();
294       } finally {
295         lock = null;
296         try {
297           channel.close();
298         } finally {
299           channel = null;
300           try {
301             f.close();
302           } finally {
303             f = null;
304             synchronized(LOCK_HELD) {
305               LOCK_HELD.remove(path.getCanonicalPath());
306             }
307           }
308         }
309       }
310       if (!path.delete())
311         throw new LockReleaseFailedException("failed to delete " + path);
312     }
313   }
314 
315   public synchronized boolean isLocked() {
316     return lock != null;
317   }
318 
319   public String toString() {
320     return "NativeFSLock@" + path;
321   }
322 
323   public void finalize() throws Throwable {
324     try {
325       if (isLocked()) {
326         release();
327       }
328     } finally {
329       super.finalize();
330     }
331   }
332 }
333