package com.fourspaces.featherdb.utils; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; /** * Lock class... Should allow the programmer to obtain a named lock (only by string or filename). They can then operate on this named entity * for as long as they want. After which, they can release the lock, and allow another section of code to obtain the lock. This is good for * intra JVM locks on "operations", not objects... for example, if you want to lock on a file, new File(filename) will give you different objects, * so you can't operate synchronized. * <p> * You can also specify a timeout for the lock, after which time it will expire and release itself. This is helpful for avoiding dead-locks. * <p> * For locks on file, it will create a absolute/file/name.lock file that can be used to lock resources outside of the JVM. * (it is removed upon release). * <p> * Note: this class doesn't enforce locks, it just ensures that concurrent threads don't try to access the same named (string id) resource * at the same time. * * @author mbreese * */ // TODO: Make this able to have shared (read-only) locks final public class Lock { private Logger log = Logger.get(Lock.class); private static Map<String,Queue<Lock>> locks = new HashMap<String,Queue<Lock>> (); private static Map<String,Lock> currentLock = new HashMap<String,Lock> (); private static Boolean processing = false; final private String key; final private long timeout; private boolean released = false; private boolean expired = false; private File lockFile = null; private Lock(String s) { this.key = s; addLock(this); this.timeout = -1; } private Lock(File f) { this.key = f.getAbsolutePath(); addLock(this); this.timeout = -1; createLockFile(); } private Lock(String s, int timeoutInterval) { this.key = s; addLock(this); this.timeout = new Date().getTime() + timeoutInterval; startWatchdog(); } private Lock(File f, int timeoutInterval) { this.key = f.getAbsolutePath(); addLock(this); this.timeout = new Date().getTime() + timeoutInterval; createLockFile(); startWatchdog(); } private void createLockFile() { lockFile = new File(key+".lock"); int timeout = 500; // wait upto 5 seconds for lock file. while (this.lockFile.exists() && timeout > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } timeout--; } try { this.lockFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } private void startWatchdog() { Thread t = new Thread(new Runnable() { public void run() { while (new Date().getTime() > timeout && !Thread.interrupted() && !released && !expired) { try { Thread.sleep(10); } catch (InterruptedException e) { } } if (!released) { expire(); } } }); t.setDaemon(true); t.run(); } /** * Releases the lock... * */ public void release() { if (!released) { log.debug("Lock on '{}' released ({})" , key,this); released=true; currentLock.remove(key); if (lockFile!=null && lockFile.exists()) { lockFile.delete(); } } } private void expire() { if (!expired && !released && timeout > 0) { log.warn("Lock on '{}' expired at {} ({})" , key,timeout,this); expired=true; release(); } } /** * Has this lock expired? * @return */ public boolean isExpired() { return expired; } /** * Has this lock been released? * @return */ public boolean isReleased() { return released; } private void addLock(Lock lock) { if (!locks.containsKey(lock.key)) { locks.put(lock.key, new ConcurrentLinkedQueue<Lock>()); } Queue<Lock> q = locks.get(lock.key); q.add(lock); waitForLock(lock); log.debug("Lock on '{}' obtained ({})" , key,lock); } private void waitForLock(Lock lock) { processQueue(); log.debug("Waiting for lock on '{}' ({})" , key,lock); while (currentLock.containsKey(lock.key) && currentLock.get(lock.key)!=lock && (lock.timeout==-1 || lock.timeout > new Date().getTime())) { try { Thread.sleep(10); } catch (InterruptedException e) { return; } } } private void processQueue() { if (!processing) { new Thread(new Runnable() { public void run() { synchronized(processing) { if (!processing) { processing = true; /* * Keep processing so long as there is a queue that still has locks pending. * For each named lock, if there isn't a currentLock, then pull the head from * the queue and make it the current lock. * * Remove keys from the lockmap that have empty queues. */ boolean found = true; while (found) { found = false; List<String> removeList = new ArrayList<String>(); for (String key: locks.keySet()) { if (!currentLock.containsKey(key)) { Lock lock = locks.get(key).poll(); if (lock!=null && (lock.timeout==-1 || lock.timeout > new Date().getTime())) { currentLock.put(key, lock); } } if (locks.get(key).isEmpty()) { removeList.add(key); } else { found = true; } } for (String key:removeList) { locks.remove(key); } if (found) { try { Thread.sleep(10); } catch (InterruptedException e) { found=false; } } } processing = false; } } } }).run(); } } /** * retrieve a named lock * @param s * @return */ public static Lock lock(String s) { return new Lock(s); } /** * retrieve a file lock * @param s * @return */ public static Lock lock(File f) { return new Lock(f); } /** * retrieve a named lock with a timeout * @param s * @param timeout - the timeout length in milliseconds * @return */ public static Lock lock(String s, int timeout) { return new Lock(s, timeout); } /** * retrieve a file lock with a timeout * @param s * @param timeout - the timeout length in milliseconds * @return */ public static Lock lock(File f, int timeout) { return new Lock(f, timeout); } }