About Jakub Holy

Jakub is an experienced Java[EE] developer working for a lean & agile consultancy in Norway. He is interested in code quality, developer productivity, testing, and in how to make projects succeed.

Simplicity vs. Robustness – Demonstrated On Lock File Handling

Today we will discuss a conflict between the design values of keeping things simple, stupid (KISS) and robustness, between underdesign and overdesign.

We were writing a batch Java application and needed to ensure that at maximum one instance is running at a time on the server. A team member had the good idea of using lock files, which indeed worked and helped us a lot. However the original implementation wasn’t very robust, which has cost us valuable people time and expensive context switches due to troubleshooting the damn application rejecting to run and locating the lock file.

As Øyvind Bakksjø of Comoyo has recently explained, a software engineer is distinguished from a mere coder by thinking and caring not only the happy path through the code but also about the unhappy cases. Good engineers think about possible problems and try to handle them gracefuly so that code that depends on them and their users have easier time dealing with problematic situation. Robustness includes catching errors early, handling them in a good way, and providing useful and helpful error messages. On the other hand, simplicity [TBD: Hickey] is a crucial characteristic of systems. It is always too easy to spend too much time on making code bullet-proof instead of focusing the effort somewhere where it would be more valuable to the business.

The overly simple implementation

The original implementation was rather simple:

public class SimpleSingletonBatchJob {
    private static boolean getLock() {
        File file = new File(LOCK_DIRECTORY+File.separatorChar+Configuration.getGroupPrefix());
        try {
            return file.createNewFile();
        } catch (IOException e) {
            return false;
        }
    }

    private static void releaseLock() {
        File file = new File(LOCK_DIRECTORY+File.separatorChar+Configuration.getGroupPrefix());
        file.delete();
    }


    public static void exit(int nr) {
        releaseLock();
        System.exit(nr);
    }

    public static void main(String[] args) throws IOException {
        ...
        if (! getLock()) { // #1 try to create lock
            System.out.println("Already running");
            return;
        }
        ... // do the job (may throw exceptions)
        
        releaseLock(); // #2 release lock when done
    }
}

The main problem is that if the app fails or is killed, it will leave the lock file behind and the next time it will reject to start with an unhelpful error message. You will need to know/read the code to understand how to fix the problem.

It has been argued that such failures and intentional kills would only happen so rarely that the effort to make the code more robust wasn’t justified. However we need to invest very little effort to make the code more friendly and robust, f.ex. by including the lock file path in the error message and explaining why it might be there and how to fix it (such as “if the app isn’t running, the lock is a leftover from a failed run and may be deleted”). Making sure that the file is removed upon failure is a few trivial lines of code that can save some confusion and time. Also, it is worthwile to make it little more robust so that it doesn’t need that many manual interventions – be nice to your ops people. (Which, I hope, is you.)

The more robust implementation

This is the improved version, with a helpful error message and lock removal upon failure:

public class RobustSingletonBatchJob {
    
    // Note: We could use File.deleteOnExit() but the docs says it is not 100% reliable and recommends to
    // use java.nio.channels.FileLock; however this code works well enough for us
    static synchronized boolean getLock() {
        File file = new File(LOCK_DIRECTORY, StaticConfiguration.getGroupPrefix());
        try {
            // Will try to create path to lockfile if it does not exist.
            file.getParentFile().mkdirs(); // #1 Create the lock dir if it doesn't exist
            if (file.createNewFile()) {
                return true;
            } else {
                log.info("Lock file " + file.getAbsolutePath() + " already exists."); // #2 Helpful error msg w/ path
                return false;
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to create lock file " + file.getAbsolutePath()
               + " due to " + e + ". Fix the problem and retry."
               , e); // #3 Helpful error message with context (file path)
        }
    }

    private synchronized static void releaseLock() {
        File file = new File(LOCK_DIRECTORY, StaticConfiguration.getGroupPrefix());
        file.delete();
    }
    
    public static void main(String[] args) throws Exception {
        boolean releaseLockUponCompletion = true;
        try {
            ...
            if (! getLock() {
                releaseLockUponCompletion = false;
                log.error("Lock file is present, exiting."); // Lock path already logged
                throw new RuntimeException("Lock file is present"); // throwing is nicer than System.exit/return
            }
            ... // do the job (may throw exceptions)
        } finally {
            if (releaseLockUponCompletion) {
                releaseLock(); // #4 Always release the lock, even upon exceptions
            }
        }
}

Improvements:

  1. Create the directory storing the locks if it doesn’t exist (its non-existence and resulting confusing error message of “Already running”) has bitten us
  2. Helpful error message “Lock file <absolute path of the file> already exists.” => easy to copy and paste int rm.
  3. Helpful error message with the file path and error when we fail to create the lock (lack of space, insufficient directory permissions, …)
  4. Wrapping the whole main into try – finally and making sure to always delete the lock file

The code still isn’t perfect – if you kill the app, the lock file will be still left behind. There are ways to handle that (f.ex. include the app’s pid in the file, upon start check not only its existence but also that the pid actually exists / is the app) but the cost of implementing them in terms of time and increased complexity is indeed higher than the benefits.

Conclusion

Both KISS and robustness are important objectives and will often be in conflict. Making your code more robust than necessary makes it too complex and costs time and has an (missed) opportunity cost. Making the code too simple costs you or its users time due to troubleshooting. Achieving the right balance requires experience and iterating towards it. If your team can’t find a consensus, it is perhaps better to start with a simpler code and collect hard data on its actual robustness needs rather then overdesigning it upfront. Don’t be perfectionist like me – but also be good to your users and fellow developers. If you can make your app more robust with little effort, do it. If it requires more work, go and collect data to justify (or not) the work.
 

Do you want to know how to develop your skillset to become a Java Rockstar?

Subscribe to our newsletter to start Rocking right now!

To get you started we give you two of our best selling eBooks for FREE!

JPA Mini Book

Learn how to leverage the power of JPA in order to create robust and flexible Java applications. With this Mini Book, you will get introduced to JPA and smoothly transition to more advanced concepts.

JVM Troubleshooting Guide

The Java virtual machine is really the foundation of any Java EE platform. Learn how to master it with this advanced guide!

Given email address is already subscribed, thank you!
Oops. Something went wrong. Please try again later.
Please provide a valid email address.
Thank you, your sign-up request was successful! Please check your e-mail inbox.
Please complete the CAPTCHA.
Please fill in the required fields.

Leave a Reply


+ eight = 10



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use | Privacy Policy | Contact
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.
Do you want to know how to develop your skillset and become a ...
Java Rockstar?

Subscribe to our newsletter to start Rocking right now!

To get you started we give you two of our best selling eBooks for FREE!

Get ready to Rock!
You can download the complementary eBooks using the links below:
Close