Why doesn't Ruby's File#flock work as expected when separate attempts are made to lock a file? Locking the file in a block is not the correct solution for this issue because the point is to show the behavior of locking on persistent locks. Using File#flock inside a block releases the lock when the block exits, so it doesn't demonstrate the problem properly.
File#flock fails in a variety of ways, especially when requesting a non-blocking lock. Some examples follow.
Infinite wait when using multiple exclusive locks, since #flock doesn't provide a way to timeout a lock request.
# First lock succeeds.
f1 = File.open('foo', File::RDWR|File::CREAT, 0644)
f1.flock(File::LOCK_EX)
# => 0
# This never returns.
f2 = File.open('foo', File::RDWR|File::CREAT, 0644)
f2.flock(File::LOCK_EX)
Asking for a non-blocking lock while the file is exclusively-locked results in an invalid argument exception.
f1 = File.open('foo', File::RDWR|File::CREAT, 0644)
f1.flock(File::LOCK_EX)
# => 0
f2 = File.open('foo', File::RDWR|File::CREAT, 0644)
f2.flock(File::LOCK_NB)
# => Errno::EINVAL: Invalid argument - foo
The documentation says that #flock "Locks or unlocks a file according to locking_constant (a logical or of the values in the table below)." However, a Logical OR raises Errno::EINVAL
or Errno::EBADF
depending on platform.
f1 = File.open('foo', File::RDWR|File::CREAT, 0644)
f1.flock(File::LOCK_EX)
# => 0
f2 = File.open('foo', File::RDWR|File::CREAT, 0644)
f2.flock(File::LOCK_NB || File::LOCK_EX)
# => Errno::EINVAL: Invalid argument - foo
While one might use the Timeout module to raise Timeout::Error
when unable to obtain an exclusive lock, it seems like File#flock ought to be able to solve this issue natively. So, how is one actually supposed to request an exclusive lock without blocking?
You can use the Timeout module to set a duration for #flock to acquire an exclusive lock. The following example will raise Timeout::Error: execution expired
, which can then be rescued in whatever way seems appropriate for the application. Returning nil when the timer expires allows the #flock expression to be tested for truth.
require 'timeout'
f1 = File.open('foo', File::RDWR|File::CREAT, 0644)
f1.flock(File::LOCK_EX)
# => 0
f2 = File.open('foo', File::RDWR|File::CREAT, 0644)
Timeout::timeout(0.001) { f2.flock(File::LOCK_EX) } rescue nil
# => nil
The documentation for File#flock says:
Locks or unlocks a file according to locking_constant (a logical or of the values in the table below). Returns false if File::LOCK_NB is specified and the operation would otherwise have blocked.
However, the method actually expects a Bitwise OR operator, rather than a Logical OR keyword as defined in parse.y by the tOROP parser token. As a result, the correct argument that allows #flock to return false
when an exclusive lock fails is actually File::LOCK_NB|File::LOCK_EX
. For example:
f1 = File.open('foo', File::RDWR|File::CREAT, 0644)
f1.flock(File::LOCK_EX|File::LOCK_NB)
# => 0
f2 = File.open('foo', File::RDWR|File::CREAT, 0644)
f2.flock(File::LOCK_NB|File::LOCK_EX)
# => false
f1.close; f2.close
# => nil
This will consistently generate an exclusive lock when available; otherwise, it immediately returns a falsy value without the overhead of raising or rescuing exceptions. This is obviously the way the module is intended to be used, but the documentation could use some clarification and additional examples to make it easier to understand.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With