Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
[egit-dev] [JGit-io-RFC-PATCH v2 4/4] Add locking capability to the IO SPI based on Java Concurrency Lock API

From: Imran M Yousuf <imyousuf@xxxxxxxxxxxxxxxxxxxxxx>

The lock implementation for local FS has 2 layer for locking, first JVM
level using the Java Concurrency API and second level is in local FS and
its Git specific *.lock files.

This change basically replaces the LockFile implementation in JGit Lib.

Signed-off-by: Imran M Yousuf <imyousuf@xxxxxxxxxxxxxxxxxxxxxx>
---
 .../src/org/eclipse/jgit/io/Entry.java             |   44 ++++-
 .../eclipse/jgit/io/localfs/LocalFileEntry.java    |  177 +++++++++++++++++-
 .../org/eclipse/jgit/io/lock/AbstractLockable.java |  199 ++++++++++++++++++++
 .../{StorageSystem.java => lock/LockManager.java}  |   96 ++++++----
 .../src/org/eclipse/jgit/io/lock/Lockable.java     |   26 ++--
 5 files changed, 486 insertions(+), 56 deletions(-)
 create mode 100644 org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/AbstractLockable.java
 copy org.eclipse.jgit.io/src/org/eclipse/jgit/io/{StorageSystem.java => lock/LockManager.java} (53%)
 copy org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryAdapter.java => org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/Lockable.java (77%)

diff --git a/org.eclipse.jgit.io/src/org/eclipse/jgit/io/Entry.java b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/Entry.java
index 5256815..4c264db 100644
--- a/org.eclipse.jgit.io/src/org/eclipse/jgit/io/Entry.java
+++ b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/Entry.java
@@ -36,6 +36,7 @@
  */
 package org.eclipse.jgit.io;
 
+import org.eclipse.jgit.io.lock.Lockable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -50,7 +51,8 @@
  * @author Imran M Yousuf (imyousuf at smartitengineering.com)
  * @since 0.6
  */
-public interface Entry {
+public interface Entry
+        extends Lockable {
 
   /**
    * Retrieves the name of the entry
@@ -138,12 +140,26 @@ public InputStream getInputStream()
           throws IOException;
 
   /**
-   * Retrieves the OutputStream for writing content into the entry. It can be
-   * opened to either overwrite it or append to it.
+   * Retrieves a locked channeled output stream. When the output stream is closed
+   * the channel is released automatically.
+   * @param overwrite False if to write in append mode else true
+   * @param lock Whether to attain lock or not
+   * @return Output stream to write content to
+   * @throws IOException If no such entry exists in append mode or there is any
+   *                     error in retrieving it or retrieving the lock.
+   */
+  public OutputStream getOutputStream(boolean overwrite,
+                                      boolean lock)
+          throws IOException;
+
+  /**
+   * Behaves in as if {@link Entry#getOutputStream(boolean, boolean)} is called
+   * with lock param as false.
    * @param overwrite False if to write in append mode else true
    * @return Output stream to write content to
    * @throws IOException If no such entry exists in append mode or there is any
    *                     error in retrieving it.
+   * @see Entry#getOutputStream(boolean, boolean) 
    */
   public OutputStream getOutputStream(boolean overwrite)
           throws IOException;
@@ -171,6 +187,28 @@ public OutputStream getOutputStream(boolean overwrite)
   public Entry getParent();
 
   /**
+   * Create this entry in the underlying system storage.
+   * @return True if created else false
+   * @throws IOException If any I/O during creation
+   */
+  public boolean createNew()
+          throws IOException;
+
+  /**
+   * Delete current entry
+   * @return True if deleted successfully else false
+   * @throws IOException If any error during writing
+   */
+  public boolean delete()
+          throws IOException;
+
+  /**
+   * Retrieve the entry for lock file
+   * @return Null if lock is attained else null
+   */
+  public Entry getLockEntry();
+
+  /**
    * Retrieve the storage system this entry either is from or will be
    * persisted to.
    * @return Storage system of the entry, will never be NULL.
diff --git a/org.eclipse.jgit.io/src/org/eclipse/jgit/io/localfs/LocalFileEntry.java b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/localfs/LocalFileEntry.java
index 4ef3076..f6c84c1 100644
--- a/org.eclipse.jgit.io/src/org/eclipse/jgit/io/localfs/LocalFileEntry.java
+++ b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/localfs/LocalFileEntry.java
@@ -45,9 +45,13 @@
 import java.io.OutputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.net.URI;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.io.Entry;
 import org.eclipse.jgit.io.StorageSystem;
 import org.eclipse.jgit.io.StorageSystemManager;
+import org.eclipse.jgit.io.lock.AbstractLockable;
 
 /**
  * Entry implementation for local file system. This class should not be
@@ -58,10 +62,15 @@
  * @since 0.6
  */
 public class LocalFileEntry
+        extends AbstractLockable
         implements Entry {
 
+  public static final String LOCK_FILE_EXT = ".lock";
+  public static final long DEFAULT_WAIT_TIME_MS = 200;
   private File localFile;
   private LocalFileSystem storageSystem;
+  private File localLockFile;
+  private String lockFileName;
 
   /**
    * Contructs an entry based of on the local file system storage and a file
@@ -152,25 +161,86 @@ public InputStream getInputStream()
     }
   }
 
-  public OutputStream getOutputStream(boolean overwrite)
+  public OutputStream getOutputStream(boolean overwrite,
+                                      boolean lock)
           throws IOException {
+    final FileOutputStream fileOutputStream;
     try {
       if (!isExists()) {
-
-        return new FileOutputStream(getLocalFile());
+        fileOutputStream = new FileOutputStream(getLocalFile());
       }
       else {
         if (overwrite) {
-          return new FileOutputStream(getLocalFile());
+          fileOutputStream = new FileOutputStream(getLocalFile());
         }
         else {
-          return new FileOutputStream(getLocalFile(), true);
+          fileOutputStream = new FileOutputStream(getLocalFile(), true);
+        }
+      }
+      if (lock) {
+        final FileLock fileLock;
+        try {
+          fileLock = fileOutputStream.getChannel().lock();
+        }
+        catch (IOException ex) {
+          fileOutputStream.close();
+          throw ex;
+        }
+        catch (Exception ex) {
+          fileOutputStream.close();
+          throw new IOException(ex);
+        }
+        if (fileLock == null) {
+          fileOutputStream.close();
+          throw new OverlappingFileLockException();
         }
+        return new OutputStream() {
+
+          @Override
+          public void write(int b)
+                  throws IOException {
+            fileOutputStream.write(b);
+          }
+
+          @Override
+          public void close()
+                  throws IOException {
+            fileLock.release();
+            fileOutputStream.close();
+          }
+
+          @Override
+          public void flush()
+                  throws IOException {
+            fileOutputStream.flush();
+          }
+
+          @Override
+          public void write(byte[] b)
+                  throws IOException {
+            fileOutputStream.write(b);
+          }
+
+          @Override
+          public void write(byte[] b,
+                            int off,
+                            int len)
+                  throws IOException {
+            fileOutputStream.write(b, off, len);
+          }
+        };
       }
+      return fileOutputStream;
     }
     catch (FileNotFoundException ex) {
       throw ex;
     }
+
+  }
+
+  public OutputStream getOutputStream(boolean overwrite)
+          throws IOException {
+    return getOutputStream(overwrite, false);
   }
 
   public Entry[] getChildren() {
@@ -262,4 +332,101 @@ public boolean setExecutable(boolean executable) {
       throw new Error(e);
     }
   }
+
+  public boolean createNew()
+          throws IOException {
+    return getLocalFile().createNewFile();
+  }
+
+  public boolean delete()
+          throws IOException {
+    return getLocalFile().delete();
+  }
+
+  public Entry getLockEntry() {
+    if (localLockFile == null) {
+      return null;
+    }
+    else {
+      return getStorageSystem().getEntry(localLockFile.toURI());
+    }
+  }
+
+  @Override
+  protected void performLock()
+          throws Exception {
+    boolean lock = performTryLock(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+    if (!lock) {
+      throw new IOException("Could not attain lock!");
+    }
+  }
+
+  @Override
+  protected boolean performTryLock(long time,
+                                   TimeUnit unit) {
+    final long milliSecTime = TimeUnit.MILLISECONDS.convert(time, unit);
+    long waitLeft = milliSecTime;
+    boolean tryAgain = true;
+    do {
+      long nextWaitDuration = Math.min(waitLeft, DEFAULT_WAIT_TIME_MS);
+      waitLeft = waitLeft - nextWaitDuration;
+      boolean success = performTryLock();
+      if (success) {
+        return success;
+      }
+      else {
+        if (nextWaitDuration > 0) {
+          try {
+            Thread.sleep(nextWaitDuration);
+          }
+          catch (InterruptedException ex) {
+          }
+        }
+        else {
+          tryAgain = false;
+        }
+      }
+    }
+    while (tryAgain);
+    return false;
+  }
+
+  @Override
+  protected boolean performTryLock() {
+    StringBuilder lockFileNameBuilder = new StringBuilder();
+    if (this.lockFileName == null) {
+      lockFileNameBuilder.append(getLocalFile().getName());
+      lockFileNameBuilder.append(LOCK_FILE_EXT);
+      this.lockFileName = lockFileNameBuilder.toString();
+    }
+    final File parent = getLocalFile().getParentFile();
+    if (parent != null) {
+      parent.mkdirs();
+    }
+    localLockFile = new File(parent, this.lockFileName);
+    if (localLockFile.exists()) {
+      localLockFile = null;
+      return false;
+    }
+    else {
+      boolean createNewFile;
+      try {
+        createNewFile = localLockFile.createNewFile();
+      }
+      catch (IOException ex) {
+        createNewFile = false;
+      }
+      if (!createNewFile) {
+        localLockFile = null;
+      }
+      return createNewFile;
+    }
+  }
+
+  @Override
+  protected void performUnlock() {
+    if (localLockFile != null) {
+      localLockFile.delete();
+    }
+  }
 }
diff --git a/org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/AbstractLockable.java b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/AbstractLockable.java
new file mode 100644
index 0000000..1e95494
--- /dev/null
+++ b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/AbstractLockable.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2009, Imran M Yousuf <imyousuf@xxxxxxxxxxxxxxxxxxxxxx>
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.io.lock;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Abstract implementation of lockable
+ * @author Imran M Yousuf (imyousuf at smartitengineering.com)
+ * @since 0.6
+ */
+public abstract class AbstractLockable
+        implements Lockable {
+
+  private ReentrantLock lock;
+  private boolean attainedCompleteLock;
+
+  protected AbstractLockable() {
+  }
+
+  @Override
+  protected void finalize()
+          throws Throwable {
+    LockManager.getInstance().unregister(getURI());
+    super.finalize();
+  }
+
+  /**
+   * Retrieve the URI of this instance.
+   * @return The URI to identify this instance. Should never be NULL
+   */
+  public abstract URI getURI();
+
+  protected boolean isInternalLockHeldOnly() {
+    return getLock().isHeldByCurrentThread();
+  }
+
+  public boolean isHeldByCurrentThread() {
+    return isInternalLockHeldOnly() && attainedCompleteLock;
+  }
+
+  public void lock() {
+    getLock().lock();
+    childLock();
+  }
+
+  public void lockInterruptibly()
+          throws InterruptedException {
+    getLock().lockInterruptibly();
+    childLock();
+  }
+
+  public Condition newCondition() {
+    return getLock().newCondition();
+  }
+
+  public boolean tryLock() {
+    if (isHeldByCurrentThread()) {
+      return true;
+    }
+    final boolean tryLock = getLock().tryLock();
+    if (tryLock) {
+      final boolean performTryLock = performTryLock();
+      if (!performTryLock) {
+        unlock();
+      }
+      else {
+        attainedCompleteLock = true;
+      }
+    }
+    return tryLock;
+  }
+
+  public boolean tryLock(long time,
+                         TimeUnit unit)
+          throws InterruptedException {
+    if (isHeldByCurrentThread()) {
+      return true;
+    }
+    final boolean tryLock;
+    long currentTime = System.currentTimeMillis();
+    tryLock = getLock().tryLock(time, unit);
+    long duration = unit.convert(System.currentTimeMillis() - currentTime,
+            TimeUnit.MILLISECONDS);
+    if (tryLock) {
+      final boolean performTryLock = performTryLock(duration, unit);
+      if (!performTryLock) {
+        unlock();
+      }
+      else {
+        attainedCompleteLock = true;
+      }
+      return performTryLock;
+    }
+    return tryLock;
+  }
+
+  public void unlock() {
+    if (isInternalLockHeldOnly()) {
+      if (attainedCompleteLock) {
+        performUnlock();
+      }
+      attainedCompleteLock = false;
+      getLock().unlock();
+    }
+  }
+
+  /**
+   * Retrieves the {@link ReentrantLock lock} based on this instances URI. It is
+   * to be noted that all instances of this URI will share the same lock.
+   * @return The lock for this lockable instance
+   */
+  protected ReentrantLock getLock() {
+    if (lock == null) {
+      lock = LockManager.getInstance().register(getURI());
+    }
+    return lock;
+  }
+
+  /**
+   * Performs additional lock operations if required by children. It will wait
+   * until it can avail for lock, but it will not wait for ever and will then
+   * throw and exception.
+   * @throws Exception If lock could be attained
+   */
+  protected abstract void performLock()
+          throws Exception;
+
+  /**
+   * Performs additional lock operations if required by children, but it will
+   * not wait for lock until beyond the time unit specified.
+   * @param time Number to wait for lock
+   * @param unit Unit of the time number
+   * @return True if lock was attained else false
+   */
+  protected abstract boolean performTryLock(long time,
+                                            TimeUnit unit);
+
+  /**
+   * Performs additional lock operations if required by children, but it will
+   * not wait for lock at all.
+   * @return True if lock was attained else false
+   */
+  protected abstract boolean performTryLock();
+
+  /**
+   * Performs additional unlock operations if required by children.
+   */
+  protected abstract void performUnlock();
+
+  private void childLock() {
+    try {
+      performLock();
+      attainedCompleteLock = true;
+    }
+    catch (Exception ex) {
+      unlock();
+      attainedCompleteLock = false;
+      throw new RuntimeException("Could not attain lock!", ex);
+    }
+  }
+}
diff --git a/org.eclipse.jgit.io/src/org/eclipse/jgit/io/StorageSystem.java b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/LockManager.java
similarity index 53%
copy from org.eclipse.jgit.io/src/org/eclipse/jgit/io/StorageSystem.java
copy to org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/LockManager.java
index 15af614..dfdcf21 100644
--- a/org.eclipse.jgit.io/src/org/eclipse/jgit/io/StorageSystem.java
+++ b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/LockManager.java
@@ -34,51 +34,77 @@
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
-package org.eclipse.jgit.io;
+package org.eclipse.jgit.io.lock;
 
 import java.net.URI;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
- * SPI providing access to the underlying storage system. Each provider is
- * differentiated using their {@link StorageSystem#getURIProtocol() URI Protocol}.
+ * Manages reentrant lock per URI
  * @author Imran M Yousuf (imyousuf at smartitengineering.com)
  * @since 0.6
  */
-public interface StorageSystem {
+public final class LockManager {
 
-  /**
-   * Returns the supported scheme of this storage system.
-   * @return Scheme supported by this storage system.
-   * @see {@link http://tr.im/BiQ0 URI Scheme}
-   */
-  public String getURIScheme();
+  private static LockManager lockManager;
 
-  /**
-   * Retrieve an entry using its URI
-   * @param uri URI to retrieve the entry for.
-   * @return Entry representing the URI
-   */
-  public Entry getEntry(URI uri);
+  public static LockManager getInstance() {
+    if (lockManager == null) {
+      lockManager = new LockManager();
+    }
+    return lockManager;
+  }
+  private final Map<URI, LockProvider> locks;
 
-  /**
-   * Retrieve the current working directory from the file system if any.
-   * @return Entry for current working directory.
-   */
-  public Entry getWorkingDirectory();
+  private LockManager() {
+    locks = new Hashtable<URI, LockProvider>();
+  }
 
-  /**
-   * Retrieve the home directory of the current user
-   * @return Home directory
-   */
-  public Entry getHomeDirectory();
+  public synchronized ReentrantLock register(URI key) {
+    if (!locks.containsKey(key)) {
+      locks.put(key, new LockProvider(
+              new ReentrantLock()));
+    }
+    return locks.get(key).get();
+  }
 
-  /**
-   * Resolve relative path with respect to a path and return the absolute
-   * entry representing the relative path.
-   * @param entry The point of reference for the relative path
-   * @param path The relative path
-   * @return The absolute entry representing the relative path entry
-   */
-  public Entry resolve(Entry entry,
-                       String path);
+  public synchronized void unregister(URI key) {
+    if (locks.containsKey(key)) {
+      LockProvider provider = locks.get(key);
+      if (provider != null) {
+        provider.decreateCount();
+        if (provider.getRegisterCount() < 1) {
+          locks.remove(key);
+        }
+      }
+    }
+  }
+
+  private static class LockProvider {
+
+    private int registerCount = 0;
+    private ReentrantLock lock;
+
+    public LockProvider(ReentrantLock lock) {
+      this.lock = lock;
+    }
+
+    public ReentrantLock get() {
+      registerCount += 1;
+      return lock;
+    }
+
+    public void decreateCount() {
+      if (lock.isHeldByCurrentThread()) {
+        lock.unlock();
+      }
+      registerCount -= 1;
+    }
+
+    public int getRegisterCount() {
+      return registerCount;
+    }
+  }
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryAdapter.java b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/Lockable.java
similarity index 77%
copy from org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryAdapter.java
copy to org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/Lockable.java
index 008fef8..d2bb039 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryAdapter.java
+++ b/org.eclipse.jgit.io/src/org/eclipse/jgit/io/lock/Lockable.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@xxxxxxxxxx>
+ * Copyright (C) 2009, Imran M Yousuf <imyousuf@xxxxxxxxxxxxxxxxxxxxxx>
  *
  * All rights reserved.
  *
@@ -34,21 +34,21 @@
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
+package org.eclipse.jgit.io.lock;
 
-package org.eclipse.jgit.lib;
+import java.util.concurrent.locks.Lock;
 
 /**
- * A default {@link RepositoryListener} that does nothing except invoke an
- * optional general method for any repository change.
+ * For objects wanting to make themselves lockable.
+ * @author Imran M Yousuf (imyousuf at smartitengineering.com)
+ * @since 0.6
  */
-public class RepositoryAdapter implements RepositoryListener {
-
-	public void indexChanged(final IndexChangedEvent e) {
-		// Empty
-	}
-
-	public void refsChanged(final RefsChangedEvent e) {
-		// Empty
-	}
+public interface Lockable
+        extends Lock {
 
+  /**
+   * Retrieves whether the current thread owns the object lock or not.
+   * @return
+   */
+  public boolean isHeldByCurrentThread();
 }
-- 
1.6.2.1



Back to the top