Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce file:lock/2 and file:unlock/1 #6251

Open
wants to merge 15 commits into
base: master
Choose a base branch
from

Conversation

lukaszsamson
Copy link
Contributor

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:

  1. https://linux.die.net/man/2/flock
  2. https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/flock.2.html
  3. http://www.polarhome.com/service/man/?qf=flock&tf=2&of=Solaris&sf=3UCB
  4. https://www.freebsd.org/cgi/man.cgi?query=flock&sektion=2
  5. https://man.openbsd.org/flock.2
  6. https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex
  7. https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-unlockfileex
  8. https://docs.microsoft.com/en-us/windows/win32/fileio/locking-and-unlocking-byte-ranges-in-files

CC @josevalim

@github-actions
Copy link
Contributor

github-actions bot commented Aug 27, 2022

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 75645a3

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

// Erlang/OTP Github Action Bot

lib/kernel/src/file.erl Outdated Show resolved Hide resolved
@josevalim
Copy link
Contributor

Awesome @lukaszsamson! This would indeed be useful for Elixir!

Some additional comments:

  • I believe you are missing adding a flock(_Device, _Mode) -> {error, enotsup}. in ram_file.erl
  • Are we missing the flock_nif/2 stub and flock/2 itself in prim_file.erl?

If the OTP team accepts this feature, there are also the following questions:

  • Should we call it file:lock/2 instead of file: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.

@rickard-green rickard-green added the team:VM Assigned to OTP team VM label Aug 29, 2022
@garazdawi
Copy link
Contributor

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 of file: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.

@lukaszsamson
Copy link
Contributor Author

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

@lukaszsamson
Copy link
Contributor Author

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.

@josevalim
Copy link
Contributor

I believe flock is more than enough for Mix/rebar3 use cases, so I am happy to go with a smaller surface area.

@lukaszsamson
Copy link
Contributor Author

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.

@garazdawi
Copy link
Contributor

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.

@lukaszsamson
Copy link
Contributor Author

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

@garazdawi
Copy link
Contributor

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?

@lukaszsamson
Copy link
Contributor Author

Nice. We need to look into some details to make the API consistent

  1. On unix subsequent calls to flock succeed even if file is already locked. I'm not sure about windows
  2. On windows it's possible to lock the same file twice (shared and exclusive). Each lock needs an unlock with matching type.
  3. 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)

@garazdawi
Copy link
Contributor

I did some testing on Windows.

  1. 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.

  1. 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.

  1. 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.

@garazdawi
Copy link
Contributor

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.

@lukaszsamson
Copy link
Contributor Author

@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?

@lukaszsamson
Copy link
Contributor Author

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.

@lukaszsamson lukaszsamson marked this pull request as ready for review January 23, 2023 07:09
@lukaszsamson lukaszsamson changed the title Introduce file:flock/2 Introduce file:lock/2 and file:unlock/1 Feb 5, 2023
@jhogberg
Copy link
Contributor

jhogberg commented Feb 6, 2023

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.

@lukaszsamson
Copy link
Contributor Author

Any updates @jhogberg @garazdawi? Is OTP27 still the target?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
team:VM Assigned to OTP team VM
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants