Using opportunistic locks to get out of the way if somebody wants the file


Opportunistic locks allow you to be notified when somebody else tries to access a file you have open. This is usually done if you want to use a file provided nobody else wants it.

For example, you might be a search indexer that wants to extract information from a file, but if somebody opens the file for writing, you don't want them to get Sharing Violation. Instead, you want to stop indexing the file and let the other person get their write access.

Or you might be a file viewer application like ildasm, and you want to let the user update the file (in ildasm's case, rebuild the assembly) even though you're viewing it. (Otherwise, they will get an error from the compiler saying "Cannot open file for output.")

Or you might be Explorer, and you want to abandon generating the preview for a file if somebody tries to delete it.

(Rats I fell into the trap of trying to motivate a Little Program.)

Okay, enough motivation. Here's the program:

#include <windows.h>
#include <winioctl.h>
#include <stdio.h>

OVERLAPPED g_o;

REQUEST_OPLOCK_INPUT_BUFFER g_inputBuffer = {
  REQUEST_OPLOCK_CURRENT_VERSION,
  sizeof(g_inputBuffer),
  OPLOCK_LEVEL_CACHE_READ | OPLOCK_LEVEL_CACHE_HANDLE,
  REQUEST_OPLOCK_INPUT_FLAG_REQUEST,
};

REQUEST_OPLOCK_OUTPUT_BUFFER g_outputBuffer = {
  REQUEST_OPLOCK_CURRENT_VERSION,
  sizeof(g_outputBuffer),
};

int __cdecl wmain(int argc, wchar_t **argv)
{
  g_o.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);

  HANDLE hFile = CreateFileW(argv[1], GENERIC_READ,
    FILE_SHARE_READ, nullptr, OPEN_EXISTING,
    FILE_FLAG_OVERLAPPED, nullptr);
  if (hFile == INVALID_HANDLE_VALUE) {
    return 0;
  }

  DeviceIoControl(hFile, FSCTL_REQUEST_OPLOCK,
      &g_inputBuffer, sizeof(g_inputBuffer),
      &g_outputBuffer, sizeof(g_outputBuffer),
      nullptr, &g_o);
  if (GetLastError() != ERROR_IO_PENDING) {
    // oplock failed
    return 0;
  }

  DWORD dwBytes;
  if (!GetOverlappedResult(hFile, &g_o, &dwBytes, TRUE)) {
    // oplock failed
    return 0;
  }

  printf("Cleaning up because somebody wants the file...\n");
  Sleep(1000); // pretend this takes some time

  printf("Closing file handle\n");
  CloseHandle(hFile);

  CloseHandle(g_o.hEvent);

  return 0;
}

Run this program with the name of an existing file on the command line, say scratch x.txt. The program will wait.

In another command window, run the command type x.txt. The program keeps waiting.

Next, run the command echo hello > x.txt. Now things get interesting.

When the command prompt opens x.txt for writing, the Device­Io­Control call completes. At this point we print the Cleaning up... message.

To simulate the program taking a little while to clean up, we sleep for one second. Observe that the command prompt has not yet returned. Instead of immediately failing the request to open for writing with a sharing violation, the kernel puts the open request on hold to give our program time to clean up and close our handle.

Finally, our simulated clean-up is complete, and we close the handle. At this point, the kernel allows the command processor to proceed and open the file for writing so it can write hello into it.

That's the basics of opportunistic locks, but your program will almost certainly not be structured this way. You will probably not wait synchronously on the overlapped I/O but rather have the completion queued up to a completion function, an I/O completion port, or have a thread pool task listen on the event handle. When you do that, remember that you need to keep the OVERLAPPED structure as well as the REQUEST_OPLOCK_INPUT_BUFFER and REQUEST_OPLOCK_OUTUT_BUFFER structures valid until the I/O completes.

(You may find the Cancel­Io function handy to try to accelerate the clean-up of the file handle and any other actions that are dependent upon it.)

You can read more about opportunistic locks on MSDN. Note that there are limitations on explicitly-managed opportunistic locks; for example, they don't work across the network.

Comments (24)
  1. Anonymous says:

    Doesn't this have some sort of race condition between CreateFileW and DeviceIOControl? What if someone tries to delete the file while you are opening it?

    When I was looking into Oplocks I found the FILE_OPEN_REQUIRING_OPLOCK flag for NtCreateFile but was put off because it's defined in winternl.h … so I wasn't sure if I should use it or not. It's documented at msdn.microsoft.com/…/bb432380%28v=vs.85%29.aspx at least, but does that mean I can/should use it?

  2. Anonymous says:

    You forgot…

    Or you might be an anti-virus vendor and you don't want to cause Visual Studio builds to randomly fail.

  3. Anonymous says:

    IIRC, oplocks are also used on the SMB/CIFS protocol, so clients can cache parts of the file and be told by the server if anything changes.

    @OldFart: not only Visual Studio builds. Other tools are also affected by programs with that broken behavior. For instance, git has code which retries a few times (with an increasing delay between each retry) when it encounters a sharing violation, and as a last resort warns the user and asks if it should retry again (compat/mingw.c on the original git if anyone wants to look; I do not know if the libgit2 Microsoft is going to use also has retry loops). I believe I saw another program also adding retry loops on sharing violations, but cannot recall which one it was at the moment.

  4. Anonymous says:

    @OldFart: What are you smoking today? Antivirus vendors don't care.

  5. Anonymous says:

    Sorry for double posting, but I just thought of this.

    The rule should be: if the file is not yours (either an internal file or something the user explicitly told you to open), you should never open it in such a way that would cause other programs to get a sharing violation error.

    Is there a wiki somewhere with a list of rules like these (which Raymond once called "taxes"; see blogs.msdn.com/…/454487.aspx)? It would be really useful, especially to people who are new to Win32 development or only develop for Win32 rarely.

  6. Anonymous says:

    I once had a program that added retry loops on sharing violations. It was horribly written and completely non-portable in that it hooked the SysMessageBox function and looked for the words "sharing violation" and then just returned the "retry" result 399 out of 400 times. (This was 16-bit Windows; I may have the name of the function wrong, and it no longer exists anyway.)

    That program was an upgrade of sorts to a hardware solution in the form of a paper clip bent into a hook that one could slide under the '5' key to hold the 'R' key down while building from an ill-conceived shared source directory in the days before widespread version control software.

  7. Anonymous says:

    Cesar: If a program opens a file for exclusive access, there's no way to open that file such that the program won't get an error. The only solution is oplocks. Of course, one could argue that in most cases programs don't need to open files for exclusive access, but that cat is already out of the bag.

  8. Anonymous says:

    Seems like the API would be simpler and free of the race condition mentioned by Zarat if CreateFile took a flag (in dwShareMode, I guess) which made kernel automatically put your handle into a "lock broken" mode when someone modifies the file. All (or most) operations on the handle would then fail with "lock broken" error code. This saves the writer waiting for the reader to stop, and frees the reader from having to with for async DeviceIoControl.

  9. jader3rd says:

    I wish that Windows implemented a way to wait for a file lock to be released. That way my code can request to open a file, but instead of an error code being returned that another process has the file opened, the call will block until I can get the file handle. It's annoying to have to write retry loops around so many calls to open a file handle.

    In between all of the Anti Virus's, Content Indexers, and anything else that might have a File System Watcher on a directory I'm adding/modifying files in, it'd be really handy to have a blocking FileOpen call.

  10. @Joshua:

    Looks like you haven't heard of lost files in VB6, caused by an antivirus interference.

    [Joshua was being sarcastic. -Raymond]
  11. Anonymous says:

    Also of note: If you don't disable oplocking, you will destroy any flat-file databases used by applications across the network, especially anything remotely related to dBase. (That means every accounting package available on the market, among other software.)

  12. Anonymous says:

    @cesar: SVN also has such a retry loop.

    Interestingly, the retry loop is only present for native Win32 builds. For cygwin builds, it is not (at least, at the time when I looked at it and even filled a bug report, some years ago).

    Unfortunately, the SVN developers did not want to add this loop for Cygwin.

    OTOH, Cygwin did not want to add such a loop generally into the system.

    Thus, the experience with cygwin was much worse than with native SVN. I had a Antivirus where you could not check out any repository that had more than some hundred files in it: You almost always got a sharing violation.

  13. Anonymous says:

    I wish that anti-virus vendors knew how to use oplocks, and that Visual Studio didn't try to patch .exe files with mt.exe after they've been created.  That's what's failing – the linker just built an .exe, so the anti-virus program sees that a new .exe file has been written, and wants to scan it.  It opens the file in order to scan it.  Meanwhile, Visual Studio uses mt.exe in order to write the manifest to the .exe's resources.  mt.exe tries to open the file for writing, but the anti-virus program is still reading the file – sharing violation.

  14. Anonymous says:

    This was the first thing I found when I searched the web on this topic:

    ‘Windows networks support a feature called ‘opportunistic locking’ which provides some performance benefits when sharing small document files.  With █████████’s database this feature provides very little benefit.  What is worse, it can lead to data corruption, especially on busy networks. If you search the internet for the phrase ‘opportunistic locking’ you will find that we are not alone in this opinion.

    To avoid data corruption you must disable the opportunistic locking feature by making changes to the registry of the file server and the workstations that access the █████████ database. If you have followed the standard █████████ installation procedures the installer will have made these changes for you.’

    If I read this correctly, the software contains a bug where it uses an optlock where it shouldn't (or vice versa) and to make up for that the installer just generally disables optlocks without telling the user. Great.

  15. Anonymous says:

    I'm more curious whether this was blocked out like so █████████ by Raymond or by AC than which program had this behavior (I didn't even bother the Google search).

    I'm of the opinion that is shared by the SQL Server team: putting databases on network fileshares is a bad idea, because we don't trust locks across the network.

  16. Anonymous says:

    @Joshua: You can find all manner of application vendors willing to blame opportunistic locking (apparently without explanation). Also, it's not just anti-virus vendors who don't care; I know of an anti-virus product whose updater was blocked by a backup application. (The backup application has since switched to using volume shadow copy so the problem no longer arises.)

  17. Anonymous says:

    I can't quite tell what problem oplocks seem to cause with databases. It seems that the problem is really with client-side caching, which is enabled by oplocks. If you disable oplocks, you effectively disable the cache. Since you have to disable oplocks to prevent the undesired behavior, people blame oplocks?

    Wouldn't using FILE_WRITE_THROUGH on your database file work just as well, though, while still allowing caching?

  18. Anonymous says:

    Joshua, I blocked it out, because the last time I mentioned another company name, it got deleted and I got a stern note from Raymond.

    Apparently Microsoft is afraid that people will sue them because of what an anonymous coward on the internet said.

    [I have been informed that I am responsible for the content of comments posted on this site. So it's not Microsoft that's afraid. It's me. -Raymond]
  19. @Anonymous Coward

    The text you posted appears in many places, including Microsoft's KB article on it's own Jet Database Engine.

  20. Anonymous says:

    Brian, I didn't know that. Oh sweet irony. Still, the problem appears to be fixed for Jet.

    But I imagine that this may not be true for all databases. What I think goes wrong is that the database reads from the locking file and sees an old version of the file that indicates that a record is unlocked and then operates on it, while in reality another client thinks it holds a lock on the record. This would obviously result in data corruption.

  21. Anonymous says:

    @Gabe

    The problem with the oplocks is that SMB and the oplocks don't guarantee consistency – of any sort. You can be in the middle of writing to the DB index, have your oplock broken by the server, have another application begin writing to the index, then complete your write at a later time, oblivious. So then you have part of the index at one point in time, and part of the index at a different point in time. Then you use the index to do a lookup, write out your data, only the index is wrong so you've just overwritten a critical data structure and hosed the db.

    In the case of a DBF, db locks are held at the end of the physical address space for a 32-bit file, and SMB ignores them for oplocking – so you can have two applications committing different data to the same row simultaneously, causing one of them to write the incorrect lengths. The next time you attempt to write to the DB, you have the same problem as earlier – since the data lengths are wrong, you overwrite a critical structure and destroy the row you were writing to, the previous row, write incorrect data to the index, and a future write will destroy the table itself.

  22. Anonymous says:

    Anon: Oplocks are designed to support caching. I don't see how that relates with your example of writing to an index, which would require file locks or byte range locks.

  23. Anonymous says:

    Well Raymond, in that case you can rest assured. You're protected by the CDA or 2000/31/EC (depending on whether people idiotic enough to sue would decide to sue in Washington or in the UK) although there is an obligation to remove illegal material upon notification (in the US this isn't well tested in court; it may not be necessary but…).

    Of course, I'm just some person on the internet. But if your lawyer tells you something different, he'd better have a damn good explanation.

    [While that prevents another company from suing me, it does not prevent Microsoft from taking action as my employer for violating company policy. -Raymond]
  24. Anonymous says:

    So it was Microsoft after all. Thought so.

Comments are closed.