Implementing a simple lock file system in Ruby



2009/09/08 // Leeds // // Feed



Lock Files are great for monitoring when a resource is in use, Wikipedia explains it simply:

File locking is often used by shell scripts and other programs: creation of lock files, which are files whose contents are irrelevant (although often one will find the Process identifier of the holder of the lock in the file) and whose only purpose is to signal by their presence that some resource is locked. A lock file is often the best approach if the resource to be controlled is not a regular file at all, so using methods for locking files does not apply. - Wikipedia

Recently whilst working on a project i needed to implement some way of monitoring when a particular task was being executed, ensuring that a duplicate task wasn’t invoked (consuming more resources and possibly hindering the execution of the current task). Ruby (and pretty much any other programming language) provides a whole host of file manipulation classes to easily implement a simple lock file system.

With Ruby we can use the File class to manipulate files, ensuring a platform independent way of reading/writing to/from the filesystem. The actual class methods that we’ll be using within the File class come from the mixin of the ftools module (which adds a group of methods for copying, moving, deleting, installing, and comparing files).

Heres the code:

# lock_file.rb
# git://github.com/joshnesbitt/lockfile.git

class LockFile
  attr_accessor :path, :filename, :qualified_path

  def initialize(path="/tmp", filename="lockfile.lock")
    @path, @filename = path, filename
  end

  def path
    @path
  end

  def filename
    @filename
  end

  def qualified_path
    "#{@path}/#{@filename}"
  end

  def process_id
    locked? ? read_lockfile(self.qualified_path) : nil
  end

  def lock!
    locked? ? false : create_lockfile(self.qualified_path)
  end

  def unlock!
    unlocked? ? false : destroy_lockfile(self.qualified_path)
  end

  def locked?
    lockfile_exists?(self.qualified_path)
  end

  def unlocked?
    !lockfile_exists?(self.qualified_path)
  end

  protected
  def lockfile_exists?(file)
    File.exists?(file)
  end

  def create_lockfile(lockfile)
    begin
      File.open(lockfile, "w") { |f| f.write(Process.pid) }
    rescue
      raise LockFileExists
    end
  end

  def read_lockfile(lockfile)
    begin
      File.open(lockfile, "r").gets { |l| puts l }
    rescue
      raise LockFileMissing
    end
  end

  def destroy_lockfile(lockfile)
    begin
      File.delete(lockfile)
    rescue
      raise LockFileMissing
    end
  end

  class FileError < StandardError #:nodoc:
  end
  class LockFileExists < FileError #:nodoc:
  end
  class LockFileMissing < FileError #:nodoc:
  end  

end

This is a very basic implementation as i didn’t need anything uber fancy. Use it like this:

l = LockFile.new("/path/to/lockfile", "test.lock")
# => "/path/to/lockfile/test.lock" containing the PID

l.locked?
# => false

l.lock!
# => Integer value

l.locked?
# => true

l.process_id
# => PID

l.unlock!
# => Integer value

l.locked?
# => false

To put it into context a bit more, imagine you’re doing something like this within your application:

def do_something
    call_rake_task
end

When hit this method will call a Rake task from within your application, lets not worry about why you might be doing this for now but focus on how we ensure this task isn’t invoked again if it’s already being executed:

def do_something
    call_rake_task unless @lockfile.locked?
end

Where you initialize the lock file instance is up to you, i ended up creating it in the initializer of the class i was using it in to make sure it was available as soon as the class was instantiated.

There are lots of uses for lock files, for me this was just one quick solution to a problem. Also its important to note that there are other solutions to performing expensive tasks outside of a typical request (most of which are better than using plain old lock files), you might want to look at these too:

Bonza!

Other (possibly related) posts

Comments