Monday, 11 June 2012

Unique Process Locking in Java

Last week I came across what is probably a very common problem: in a managed language, such as Java, how does one ensure that only one instance of an application is running at once? When using, for example, the Windows API, we can use the CreateMutex method and check if that has been locked, but no such sharing exists in Java. Furthermore, our requirements specified that we would be using the same application but simply with a different configuration. 


Two solutions presented themselves, both of which have been around forever. Firstly, locking with a socket - a very simple solution, try to bind to a socket, if it's in use, assume the process is already running. Care has to be taken to retain a reference to the socket so we can clean it up later, but also in case the garbage collector reclaims it before the socket is opened. 


I have two issues with this approach, which are very closely related. Firstly, how do we choose a port which we can guarantee isn't in use by another application? We can't - although we can make the port configurable to alleviate the problem. Secondly, what if we want to have more applications running than we have available ports? Whilst this might seem far-fetched, once you account for the root port range (1-1023) and the ephemeral port range (varies depending on OS, usually 32768-61000 on Linux although IANA recommend 49152-65535), you can quickly see how the port numbers can run out - especially if you're trying to lock say a web service (although, I guess you could use your web service port as your locking port).


The second approach is to use a FileLock object from Java 1.4's NIO package. I prefer this approach as it doesn't suffer from the drawbacks of the socket approach. It does carry its own flaws - we may not have a filesystem to write to, for example - but these scenarios are much less common. 


Code for both methods is below. If anyone knows a good code formatter I can use for hosted Blogger, let me know!


Exceptions
ProcessLockException: General exception thrown by the underlying locking mechanism when attempting to lock.
AlreadyLockedException: Thrown when the locking mechanism has already locked in this instance.
NotLockedException: Thrown when an attempt is made to unlock an open lock.


ProcessLocker Interface

public interface ProcessLocker {
    public boolean lock() throws ProcessLockException;


    public void unlock() throws NotLockedException;
}

File Lock Interface
import java.beans.ConstructorProperties;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileChannelProcessLocker implements ProcessLocker {
    private final String filename;

    private FileOutputStream outputStream;
    private FileLock lock = null;

    @ConstructorProperties({ "filename" })
    public FileChannelProcessLocker(String filename) {
        super();
        this.filename = filename;
    }

    @Override
    public boolean lock() throws ProcessLockException {
        if (lock != null) {
            throw new AlreadyLockedException();
        }

        File file = new File(filename);
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                throw new ProcessLockException("Couldn't create locking file.", e);
            }
        }

        try {
            outputStream = new FileOutputStream(file);
        } catch (FileNotFoundException e) {
            throw new ProcessLockException("Couldn't open the lock file.", e);
        }

        FileChannel fileLockChannel = outputStream.getChannel();

        try {
            lock = fileLockChannel.tryLock();
        } catch (IOException e) {
            throw new ProcessLockException("Couldn't lock the locking file.", e);
        }

        if (lock == null) {
            return false;
        }

        return true;
    }

    @Override
    public void unlock() throws NotLockedException {
        if (lock == null) {
            throw new NotLockedException();
        }

        try {
            lock.release();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String getFilename() {
        return filename;
    }
}

Socket Locking Interface
import java.io.IOException;
import java.net.BindException;
import java.net.ServerSocket;

public class SocketProcessLocker implements ProcessLocker {
    private final int portNumber;

    private ServerSocket socket = null;

    public SocketProcessLocker(int portNumber) {
        super();
        this.portNumber = portNumber;
    }

    @Override
    public boolean lock() throws ProcessLockException {
        if (socket != null) {
            throw new AlreadyLockedException();
        }

        try {
            socket = new ServerSocket(portNumber);
        } catch (BindException e) {
            // Most bind exceptions are because the port is already in use
            return false;
        } catch (IOException e) {
            throw new ProcessLockException("Couldn't bind to socket.", e);
        }

        return true;
    }

    @Override
    public void unlock() throws NotLockedException {
        if (socket == null) {
            throw new NotLockedException();
        }

        try {
            socket.close();
        } catch (IOException e) {
            // Might want to rethrow this
            e.printStackTrace();
        } finally {
            socket = null;
        }
    }
}