9

I am having concurrency issues between two processes after short research I have seen that temporary file is suggested solution to this problem.

So solution would be to create /tmp/global.lock and use it as global lock. Example of this I have found in this thread Mutex for Rails Processes

Make sense to me so far, but I would like to see best practice for this solution. Above explained make sense but I wonder how to check if given file is locked?

fh = File.open("/some/file/path", File::CREAT)

begin
  if locked = check_file_locked?
    sleep(1)
  else
    fh.flock(File::LOCK_EX)
    # do what you need to do
  end
ensure
  fh.flock(File::LOCK_UN)
end

This is my understanding of solution and not sure how to implement mentioned check_file_locked?()? Also if there is best way would love to hear it.

Haris Krajina
  • 14,824
  • 12
  • 64
  • 81

3 Answers3

10

@bjhaid's answer can cause a problem with Timeout#timeout causing an interpreter error in Rubinius. It's also unnecessarily complicated.

Here's a simpler version, using a nonblocking lock instead of timeout:

def locked? lockfile_name
  f = File.open(lockfile_name, File::CREAT)

  # returns false if already locked, 0 if not
  ret = f.flock(File::LOCK_EX|File::LOCK_NB)

  # unlocks if possible, for cleanup; this is a noop if lock not acquired
  f.flock(File::LOCK_UN) 

  f.close
  !ret # ret == false means we *couldn't* get a lock, i.e. it was locked
end
Sai
  • 6,919
  • 6
  • 42
  • 54
  • 4
    The lock will be released as soon as you close the file. Consequently, if you use the block syntax of open (`open(path) do |f|...end`), you can remove both the `LOCK_UN` operation and the explicit close. Furthermore, if use you use the block from, the lock will be released even if an exception occurs. – hagello Jan 14 '19 at 10:51
8

When you have an exclusive lock to a file, attempting to lock it again in ruby would wait indefinitely till the file is unlocked, so you can rely on that and set a timeout on how long ruby should would wait, this might not be the most adequate way but I would do as below:

fh = File.open("/some/file/path", File::CREAT)
fh.flock(File::LOCK_EX)

require 'timeout'
def check_file_locked?(file)
  f = File.open(file, File::CREAT)
  Timeout::timeout(0.001) { f.flock(File::LOCK_EX) }
  f.flock(File::LOCK_UN)
  false
rescue 
  true
ensure
  f.close
end
f = File.open("/tmp/a.txt", "w+")
f.flock(File::LOCK_EX)
check_file_locked?("/tmp/a.txt") # => true
f.flock(File::LOCK_UN)
check_file_locked?("/tmp/a.txt") # => false 
bjhaid
  • 9,592
  • 2
  • 37
  • 47
  • 3
    This is bad use of a magic number (`0.001`). Why is it exactly this value? Why isn't it any bigger or smaller? This arbitrary constant should at least be explained. – hagello Jan 14 '19 at 10:45
  • 3
    The proper interface for acquiring a lock in an non-blocking way is `File::LOCK_NB`. You have to do a logical or with the desired operation. Example: `flock(File::LOCK_EX | File::LOCK_NB)`. Avoid workarounds! Furthermore, `LOCK_NB` is way faster than an way of doing it with a timeout. – hagello Jan 14 '19 at 10:49
  • This magic number is in a sleep. Explaining or worse adding a constant would add a lot more of a mental model and make the code more unreadable. Welcome to the real world. The real problem with the code is that "timeout" is a terrible implemented feature in ruby and should be avoid as much as possible. – Lothar Mar 19 '23 at 08:51
0

I made a simple Mutex class for this

class CrossProcessMutex
  def initialize(lock_filepath)
    @lock_filepath = lock_filepath
  end

  def synchronize
    # https://ruby-doc.org/3.2.0/File.html#method-i-flock
    f = File.open(@lock_filepath, File::RDWR|File::CREAT, 0644)
    f.flock File::LOCK_EX
    yield
  ensure
    f.flock File::LOCK_UN
  end
end