otp
otp copied to clipboard
Introduce file:flock/2
This PR extends file
module with new function flock/2
. It allows locking files for shared or exclusive access on Unix and Windows.
Rationale: Currently the OTP lacks suitable APIs allowing fine grained control over access to filesystem. Those can be especially useful for applications like language tools, compilers (e.g. elixir compiler), IDEs (like language servers) that share access to the same filesystem directories.
Please note that this is a first draft that received only a little testing on macOS platform. I would like to get some guidance if the direction I took is acceptable and if the OTP Team would be interested in merging this. The documentation will also need further improvements.
Related manpages and documentation:
- https://linux.die.net/man/2/flock
- https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/flock.2.html
- http://www.polarhome.com/service/man/?qf=flock&tf=2&of=Solaris&sf=3UCB
- https://www.freebsd.org/cgi/man.cgi?query=flock&sektion=2
- https://man.openbsd.org/flock.2
- https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex
- https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-unlockfileex
- https://docs.microsoft.com/en-us/windows/win32/fileio/locking-and-unlocking-byte-ranges-in-files
CC @josevalim
CT Test Results
No tests were run for this PR. This is either because the build failed, or the PR is based on a branch without GH actions tests configured.
Results for commit 75645a319f7c91c331912567dda0eeb92699977b
To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.
See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.
Artifacts
- Complete CT logs (Download Logs)
- No HTML docs found
- No Windows Installer found
// Erlang/OTP Github Action Bot
Awesome @lukaszsamson! This would indeed be useful for Elixir!
Some additional comments:
- I believe you are missing adding a
flock(_Device, _Mode) -> {error, enotsup}.
inram_file.erl
- Are we missing the
flock_nif/2
stub andflock/2
itself inprim_file.erl
?
If the OTP team accepts this feature, there are also the following questions:
- Should we call it
file:lock/2
instead offile:flock/2
? - Should the unlock flag be its own function instead (
file:unlock/2
)? - Should we default to "exclusive" mode and then only provide shared as an option?
I would answer yes to all of the above but it is up to the OTP team, so I would wait for feedback before further changes.
Hello!
We will need to have an OTB (OTP Technical Board) meeting to talk about if and how we want to have this, but since it can be implemented on all supported platforms I don't see any reason why we would not want it.
- Should we call it
file:lock/2
instead offile:flock/2
?- Should the unlock flag be its own function instead (
file:unlock/2
)?- Should we default to "exclusive" mode and then only provide shared as an option?
I would answer yes to all of the above but it is up to the OTP team, so I would wait for feedback before further changes.
I would also say yes to all of the above.
Here's a build from a branch with updated preloaded (a check in kernel is failing as my machine differs from the one running GHA) https://github.com/lukaszsamson/otp/runs/8089001687?check_suite_focus=true
Another thing to consider is if we would like to support only entire file locking (via BSD flock) or support range locking (via POSIX lockf or fcntl). I guess open file description locks via fcntl are out of the box as this is Linux only. Here's a nice summary of pros and cons of different Unix options (https://gavv.net/articles/file-locks/). Windows LockFileEx does support range locking.
I believe flock
is more than enough for Mix/rebar3 use cases, so I am happy to go with a smaller surface area.
Should the unlock flag be its own function instead (
file:unlock/2
)?
Then maybe file:unlock/1
. On unix it would be trivial to implement with flock
syscall. The windows implementation would need to unlock both shared and exclusive locks but it would make the behaviour on this platform closer to unix one.
And regarding windows, maybe we shouldn't let shared and exclusive locks over the same file and just unlock one when file:lock/2
is called again with other type.
I started looking at this PR as we will soon have a meeting to discuss it. I'm putting some notes here in case you have any opinions.
It would seem like Windows has decided to make the default shared
. From what I can tell python+ruby+java follow the unix default (i.e. you have to specify what you want), while rust has created different functions (i.e. lock_exclusive
and lock_shared
). So it would seem like most/all other languages have chosen to make it mandatory to specify which mode you want. Maybe that is a reason for us to do the same...
I don't think we should support locking byte ranges. If we need that we can add it in another PR.
It is too bad that there is no asynchronous API that we can use. Blocking a dirty_io_scheduler
while waiting for a lock to be released is not ideal, and neither is polling for the lock to be released. Though there is not much we can do about it.
It is too bad that there is no asynchronous API that we can use
Windows API can be called asynchronously with IO completion ports. On unix fcntl has some async support but I couldn’t find anything about async locks
Hello!
We have (finally) made a decision and we would like this functionality. We propose that the API should be:
-spec file:lock(file:io_device(), [shared | exclusive | non_blocking]) ->
ok | {error, already_locked | file:posix()}.
-spec file:unlock(file:io_device()) -> ok | {error, terminated}.
I think that is the same as what has been discussed in this PR?
Nice. We need to look into some details to make the API consistent
- On unix subsequent calls to flock succeed even if file is already locked. I'm not sure about windows
- On windows it's possible to lock the same file twice (shared and exclusive). Each lock needs an unlock with matching type.
- I'm not sure what's the effect of unlocking an unlocked file on windows. On unix it succeedes without error (if the file descriptor is valid)
I did some testing on Windows.
- On unix subsequent calls to flock succeed even if file is already locked. I'm not sure about windows.
Windows will fail with ERROR_LOCK_VIOLATION
if file is already locked using the same HANDLE. I would suggest that we keep some metadata about whether the file is locked or not and with which lock type so that we can just return success if something is already locked.
- On windows it's possible to lock the same file twice (shared and exclusive). Each lock needs an unlock with matching type.
On Windows you cannot upgrade a lock, but you can downgrade. That is you can do:
FileLockEx(handle, EXCLUSIVE);
FileLockEx(handle, SHARED);
FileUnlockEx(handle, EXCLUSIVE);
but you cannot do:
FileLockEx(handle, SHARED);
FileLockEx(handle, EXCLUSIVE); // this will block or return ERROR_LOCK_VIOLATION
FileUnlockEx(handle, SHARED);
Also, you can do as many shared locks are you like, but you need to do the same amount of unlocks for all the locks to be released. That is:
FileLockEx(handle, SHARED);
FileLockEx(handle, SHARED);
FileLockEx(handle, SHARED);
FileLockEx(handle, SHARED);
FileUnlockEx(handle, SHARED);
FileLockEx(handle, EXCLUSIVE|NB); // ERROR_LOCK_VIOLATION
FileUnlockEx(handle, SHARED);
FileLockEx(handle, EXCLUSIVE|NB); // ERROR_LOCK_VIOLATION
FileUnlockEx(handle, SHARED);
FileLockEx(handle, EXCLUSIVE|NB); // ERROR_LOCK_VIOLATION
FileUnlockEx(handle, SHARED);
FileLockEx(handle, EXCLUSIVE|NB); // Success
Here again, I think it would be good to keep a flag in the file structure so that any extra locks are just ignored so that we behave as on Unix.
- I'm not sure what's the effect of unlocking an unlocked file on windows. On unix it succeedes without error (if the file descriptor is valid)
You get ERROR_NOT_LOCKED
on Windows.
Also locks on Windows are not only advisory, so if you do FileLockEx(handle, EXCLUSIVE)
you cannot do file:read_file(File)
on that file from the same or another process.
@garazdawi I applied your suggestions and split flock/2
into lock/2
and unlock/1
. I was able to test POSIX implementation on macos, will try to set up windows machine later.
I'm not sure about already_locked
error. Do we need it? If we'd like to ignore extra locks on windows than probably not.
On Windows you cannot upgrade a lock, but you can downgrade
Do you think we should make it closer to POSIX, i.e. add support for nonatomic lock upgrade and make downgrade nonatomic?
I made changes to Windows implementation to match unix. If a lock of a different type is already held then unlock is called before attempting to lock.
We had another technical board meeting today to try to reconcile the differences between Unix and Windows, and have realized that the functionality differs too much between operating systems to pretend that it's the same function. There's no good way to compensate for this in Erlang. :-(
We think it's time for us to refactor the file system support in Erlang by dividing it into several modules, one for generic and actually platform independent operations, and other modules for platform specific ones such as Windows, POSIX, BSD, Linux (which I suppose would cover those not in POSIX).
This is something we've been thinking about for a long time but haven't had enough motivation for until now. We plan to get this done early in OTP 27 and we'll bring this PR in with it.
Any updates @jhogberg @garazdawi? Is OTP27 still the target?