Enable Cross-platform File Locking with a Lock Server

Enable Cross-platform File Locking with a Lock Server

JDK 1.4 adds a number of advanced features related to I/O and filesystems. The more advanced I/O features are often non-portable or only partially portable. File locking is one of these features. While most operating systems provide some form of file locking, file locking isn’t implemented exactly the same way on every platform.

For a general example of file locking in Java, review the following code snippet. It locks the first 10 bytes of a file called “foo.txt”:

RandomAccessFile raf =  new RandomAccessFile( "foo.txt", "rw" );FileChannel fc = raf.getChannel();FileLock lock = fc.lock( 0, 10, false );

This is an exclusive lock, which means that no one else can acquire a lock on this same region until it is released:

lock.release();

The idea is that only one exclusive lock can exist on any particular region of a file at any given time. You cannot acquire an exclusive lock on a region that overlaps a region that is already locked with another exclusive lock (see Figure 1).

Figure 1: Locking Conflicts

This file-locking facility also provides shared locks, which allow more than one shared lock on the same region of a file?as long as no exclusive locks are overlapping the same region.

Platform Differences
Because platforms don’t all implement file locking in the same way, the JDK file-locking facility is not identical on each platform. It may have a number of differences:

  • Locks may or may not be advisory. An advisory lock does not actually prevent access to the thing locked, it prevents only other locks from being acquired. This may sound confusing, but it’s probably familiar to you?the locks used in Java for the synchronized keyword are advisory locks.
  • Shared locks may not be available. A platform might have only exclusive locks. For many applications, this is okay. The only side effect is that some systems run more slowly. Strictly speaking, however, shared and exclusive locks have different semantics so this isn’t a trivial point.
  • Locks may be process-specific. Locks held in one process may or may not affect locking activity in other processes. While it is increasingly common for locks to be system-wide, locks may be only process-wide in older operating systems.

In a practical sense, the last of these three issues is the most immediate. Locks are most often used to prevent data corruption in files to which multiple entities are writing. If locks are only process-wide, then multiple processes can potentially corrupt data. I’ll examine a solution to this problem in the following sections.Create a Lock Server
If an implementation?whether the operating system or the Java implementation?does not provide system-wide lock safety, it’s because that implementation does not have a central facility for managing locks for the entire system. In a sense, each process is left to coordinate locking activities on it’s own (see Figure 2).

Figure 2: Process-wide Locking

To remedy this, you can create your own central facility: a lock server. Every program that uses locks will connect to and request locks from the lock server. The server will handle the locking logic that the underlying system does not provide. All lock and release requests must go through this central server. Figure 3 shows the lock server structure.

Figure 3: A Lock Server

The lock server uses the implementation that the underlying system provides to implement the locking logic. All locking activity goes through this central facility. In this case, process-wide locking is sufficient because only one process actually is doing any locking?the lock server itself.

The Lock Server’s Required Classes
The lock server implementation I describe will use the following classes:

  • LockServer.java. This is the central server that provides lock services to other processes. It listens for incoming connections on a specified port and responds to lock and release requests from the clients at the other end of those connections.
  • RemoteLockClient.java. A program uses this class on the client side to make use of the locking services a lock server provides. This object establishes a connection to the server and handles the details of communicating with the server.
  • RemoteLock.java. This class is the equivalent of java.nio.channels.FileLock. It represents a single lock on a particular region of a particular file.
  • RemoteLockException.java. This class is used for exceptions pertaining to the remote locking protocol.
  • RemoteLockConstants.java. This class contains some constants used in the communications protocol between the client and server.

Using RemoteLocks
As I mentioned earlier, you acquire a regular lock via the FileChannel object associated with a file:

RandomAccessFile raf =  new RandomAccessFile( "foo.txt", "rw" );FileChannel fc = raf.getChannel();FileLock lock = fc.lock( 0, 10, false );

In contrast, you get RemoteLocks from a RemoteLockClient object. When you create the RemoteLockClient object, you need to specify the hostname and port number:

RemoteLockClient client = new RemoteLockClient( hostname, port );RemoteLock rl = client.lock( new File( filename ), position, size, false );

Note also that you must specify the file (via a File object) when calling lock(). This is because you are getting your lock from a RemoteLockClient, which is used for all locks you will acquire, regardless of the file on which you acquire them.

Releasing a RemoteLock is just like releasing a regular lock:

rl.release();

The first thing the client has to do is create a RemoteLockClient, which connects to the server:

public RemoteLockClient( String hostname, int port ) {  this.hostname = hostname;  this.port = port;  try {    socket = new Socket( hostname, port );    // ...  } catch( IOException ie ) {    throw new RuntimeException(      "Cannot connect to lock server "+      hostname+":"+port );  }}

The boldfaced line is where the action is?where the RemoteLockClient connects to the lock server.

Now it’s time to acquire a lock. You’ll notice that java.nio.channels.FileLock has four different locking methods, divided into two pairs of two. In the pair called lock(), one method locks a particular region of a file, and the other one locks the entire file. The second pair, called tryLock(), does the same but in a non-blocking fashion. If the lock cannot be acquired, instead of blocking these two methods return null.

Likewise, RemoteLockClient has four methods. However, each of these in turns calls a single, protected method called doLock(). Figure 4 shows the relationship between these methods.

Figure 4: The ‘World of Sound’ Sample Store

The doLock() method marshals its arguments into a lock request and sends them down the wire to the lock server:

dout.writeInt( ACQUIRE_LOCK );dout.writeUTF( filename );dout.writeLong( position );dout.writeLong( size );dout.writeBoolean( shared );dout.writeBoolean( doTry );

After this information is sent, the server responds with a token that distinguishes this lock from other locks:

int token = din.readInt();

This token is associated with the FileLock object in a Map called tokenToLock. This token is also stored inside the RemoteLock object returned by doLock(). Later, when the RemoteLock’s release() method is called, this token is sent back to the server inside a release request:

dout.writeInt( RELEASE_LOCK );dout.writeInt( token );

The server responds with a code, which is either RELEASE_OK or RELEASE_ERROR:

int response = din.readInt();

If the response is RELEASE_ERROR, the client throws a RemoteLockException.

Meanwhile, on the server side, the server accepts new connections in a background thread. It hands each incoming connection off to a ClientHandler object, which is static to the LockServer class. Each ClientHandler also spawns a background thread, which responds to requests from the client.

First, the ClientHandler finds out whether the incoming request is a lock request or a release request:

int code = din.readInt();if (code==ACQUIRE_LOCK) {  // ...} else {  // ...}

If it’s a lock request, you read the arguments and attempt to acquire the lock. Note that how you do this depends on the values of two flags: shared and doTry. Shared specifies whether the lock should be shared or not, while doTry specifies whether you should block or return null in the event that you cannot immediately acquire the lock:

RandomAccessFile raf =  new RandomAccessFile( filename,    shared ? "r" : "rw" );FileChannel fc = raf.getChannel();FileLock lock = null;if (doTry) {  lock = fc.tryLock( position, size, shared );} else {  lock = fc.lock( position, size, shared );}

Note that you have to open the file either read-only or read/write, depending on whether the lock is exclusive or shared (respectively). If you do acquire the lock, you must generate a token for it and send that token back to the client:

token = getSerial();// ...dout.writeInt( token );

If on the other hand the request is a release request, you get the FileLock object from the tokenToLock map and release the lock. You also send back a value indicating whether the release action succeeded or not:

int token = din.readInt();FileLock lock =  (FileLock)tokenToLock.get(    new Integer( token ) );lock.release();// ok is set to false if an error occursdout.writeInt(  ok ? RELEASE_OK : RELEASE_ERROR );

See the full source listings for more details.Start the server, if it isn’t already running, and specify a port on the command line:

$ java LockServer 5678Listening on port 5678

Once your server is running, you can run the test program called Test.java. This program lets you specify the various locking parameters on the command line, like this:

$ java Test        

The test program will acquire the lock if it can, and then wait for you to press Return. When you press return, it will release the lock. Try running multiple copies of Test in different windows to see the locks interact.

Reconcile Cross-platform Differences
A lock server can make up for differences between operating systems or Java implementations. The file-locking specification allows each implementation to choose whether its locks are process-wide or system-wide, which can play havoc with a program that needs system-wide locks. In these situations, you can use a lock server to replace the functionality missing from the underlying implementation.

This approach is an excellent way to deal with cross-platform differences. Just write it yourself! If you run your program on a system that happens to have system-wide locking, the lock server will still work correctly.

Share the Post:
data observability

Data Observability Explained

Data is the lifeblood of any successful business, as it is the driving force behind critical decision-making, insight generation, and strategic development. However, due to its intricate nature, ensuring the

Heading photo, Metadata.

What is Metadata?

What is metadata? Well, It’s an odd concept to wrap your head around. Metadata is essentially the secondary layer of data that tracks details about the “regular” data. The regular