How to exclusively lock file access in ruby

We will achieve this by creating a block accepting method to optionally create and then lock a .lock File of the underlying accessed file.

Why create a .lock file?

  • The main advantage of creating a .lock file is that #flock might block some operations and require the index node of the file to be consistent. Some operations might change that index node.
  • In some cases it might also be convenient to just read/write the lock file first and update the other file afterwards or vice versa, such that breaking of a process does not affect the other version.

That being said the shown process also works without creating a lock file by calling File.open(@file_path) { ... } on the file directly instead. I will also include an example on how to use this for directories instead for files at the end.

The method

You can define the following method to lock a file from read and write access by methods like File.open and File.read.

def exclusively_locked_access
  begin
    @file_locked = true
    File.open(F"#{file_path}.lock"), 'w+') do |f|
      f.flock(File::LOCK_EX)
      yield
    end
  ensure
    @file_locked = false
  end
end
  • #flock requests a lock and waits until it can lock the file.
    • Note that exiting the block from open will also unlock the file.
  • The passed LOCK_EX for flock will create an exclusive lock for that file. You can look up the doc for #flock Show archive.org snapshot to find all available locking options.

Now we can pass any block to the method to safely every file access you want to have locked like so

exclusively_locked_access do
  file_content = File.read(@file_path)
end

Variations

When saving the file in a variable

  • Locking the file in the block passed to File.open will take care of closing and unlocking the file.
  • If you don't use that block make sure to do that manually and preferably wrap it it with begin and rescue if something might go wrong:
begin
  f = File.open(file, File::CREAT)
  f.flock(File::LOCK_EX)
  yield
ensure
  f.flock(File::LOCK_UN)
  f.close
end

Waiting for a timeout

If you might run into deadlocks it can be safer to ensure a timeout with using Timeout::timeout(0.1) { f.flock(File::LOCK_EX) }

Locking directories

  • If one process might change several files within one folder you can indeed make the method from above write in a way that it accepts the path.
  • But when files are read and written at the same time and their content is changed in dependent on each other, it might be a better idea to flock the complete folder instead.

This can also achieved similarly:

def exclusively_folder_locked_access
  directory = File.open(Paths::GEM_STORAGE)
  directory.flock(File::LOCK_EX)
  yield
  directory.flock(File::LOCK_UN)
end

Since you can't write a directory you also don't have to call #close on it.

Felix Eschey About 1 year ago