Opened 19 years ago
Last modified 2 weeks ago
#229 assigned defect
Stub databases should be read with msvc_posix_open
| Reported by: | Richard Boulton | Owned by: | Olly Betts |
|---|---|---|---|
| Priority: | normal | Milestone: | 2.1.1 |
| Component: | Other | Version: | git main |
| Severity: | normal | Keywords: | |
| Cc: | Olly Betts | Blocked By: | |
| Blocking: | Operating System: | Microsoft Windows |
Description (last modified by )
Currently, stub databases are read using a standard C++ ifstream. (See backends/database.cc, function open_stub()) This works fine, except that if a user (or the database replication code) tries, on Windows, to atomically rename a new stub db file over an existing one, it will receive an error if the old stub DB file was open.
This can be avoided if we instead use msvc_posix_open() (or just open() on unix) in open_stub() to get a file handle for the stub database, and access it using C file-handling routines.
Change History (12)
comment:1 by , 19 years ago
| Status: | new → assigned |
|---|
comment:2 by , 19 years ago
| Cc: | added |
|---|---|
| Operating System: | → Microsoft Windows |
comment:3 by , 19 years ago
So, a full fix would probably have to handle failures of the read, and retry. What a pain.
comment:4 by , 19 years ago
Ah yes, that should work if the read returns a suitable error (which I think it does).
comment:6 by , 15 years ago
| Description: | modified (diff) |
|---|
For MSVC it looks like we could just write:
ifstream stub(msvc_posix_open(file.c_str(), O_RDONLY));
But GCC's libstdc++ doesn't have this non-standard form, so we can't use this for mingw, but there is stdio_filebuf, which allows you to wrap an fd or FILE* as an istream or ostream. This would allow us to keep the current code with only minor changes on the affected platform.
We should perhaps check if iostream imposes an overhead over the C FILE* routines, and if there is much of one have a policy to avoid istream and ostream instead.
Also while one option is to retry the whole stub read upon read failing, another is to simply require that the stub update attempt retries. It is perhaps better to have the writer blocked by heavy reader activity than have readers blocked by heavy writer activity as readers tend to be more performance sensitive.
comment:7 by , 10 years ago
| Milestone: | → 1.4.x |
|---|
comment:8 by , 10 years ago
| Owner: | changed from to |
|---|---|
| Status: | assigned → new |
comment:9 by , 4 years ago
| Milestone: | 1.4.x → 2.0.0 |
|---|---|
| Version: | SVN trunk → git master |
comment:11 by , 4 weeks ago
| Milestone: | 3.0.0 → 2.x |
|---|---|
| Status: | new → assigned |
While looking at fixing #854, we ideally want to read the stub file from a file descriptor. In the case where we've been asked to open a database passing in a path which is a file and that file is large enough that it could be a single file database (which means at least 2KB for glass and honey) we open an fd on it to look at the file magic to see if it is, and if not we currently close the fd and open the file again as a std::ifstream.
That means there's a race condition if you replace a >= 2KB stub file with a single file database - if you're very unlucky with the timing, a reader could sniff the stub file, close it, another process updates the filename to be a single file database, and the reader opens that as a std::ifstream and tries to parse it as a stub file.
I think there's also a race for a smaller stub file - as we could stat() the stub file, another process updates the filename to be a single file database, and the reader opens that as a std::ifstream and tries to parse it as a stub file. We should always open as a file descriptor first and use fstat() on that to decide if we can skip checking the file magic.
(It's also slightly inefficient to open the file twice, though I suspect most stub files are under 2KB so currently only get opened as a std::ifstream.)
That requires moving to reading stub files from a file descriptor, so as part of that we can address this issue by using posixy_open() (which replaced msvc_posix_open() back in 2012, and is just an alias to open() on Unix-like platforms). We probably just need to ensure that all reading of stub files happens this way (there's some in replication for example; ideally they should probably share more code anyway).
comment:12 by , 2 weeks ago
| Milestone: | 2.x → 2.1.1 |
|---|
Fixed by add5c798b671e470be8e09d910463d193d3b6a0e.
So, a full fix would probably have to handle failures of the read, and retry. What a pain.
I've not attempted to handle retrying reading the stub file in xapian-core. Perhaps we should, but at least for now user code will need to handle the retry.
comment:13 by , 2 weeks ago
So, a full fix would probably have to handle failures of the read, and retry. What a pain.
I had a quick poke at this in CI: https://github.com/ojwb/test-stuff/actions
It seems with modern Microsoft Windows versions that the file remains readable after deletion. I found this interesting comment which appears to be from someone at Microsoft: https://github.com/golang/go/issues/32088#issuecomment-502850674
in the most recent version of Windows, we updated
DeleteFile(on NTFS) to perform a "POSIX" delete, where the file is removed from the namespace immediately instead of waiting for all open handles to the file to be closed. It still respectsFILE_SHARE_DELETE, but now otherwise behaves more like POSIX unlink. This functionality was added for WSL and was considered worth using by default for Windows software, too.
I have to say it's refreshing for Microsoft to be making things more compatible rather than less!
Some background to that comment is that some places suggest you should rename a file to a random name (and probably make it hidden) before deleting it because on older OS versions the file actually continues to exist for a while and that causes issues if you try to create a file with the same name as the deleted one. I've seen suggestions to rename into a higher level directory to avoid blocking the removal of the containing directory. Creating randomly named hidden files in the root directory of a volume seems really icky though.
That comment doesn't explicitly say, but my testing shows that open handles also continue to work on such a deleted file. Things may be less rosy when not using NTFS though - I don't think I can easily test that via GHA.
I also tested atomic rename in that repo's CI. It seems this fails with ERROR_ACCESS_DENIED if there's an open handle on the target, even if that handle was opened using the posixy_open() approach. This affects using posixy_rename() to atomically update files, but it's the writer not the reader that is affected. I think this is actually more problematic that the stub update problem as it affects the mechanism we use during WritableDatabase::commit() to update the version file. Experimenting it seems we can solve this by having posixy_rename() first try to use ReplaceFileA() which works even if the target is open; it fails unless the target already exists, so we then need to fall back to MoveFileEx(). (This doesn't work under Wine for some reason, which we use for CI - I think we don't exercise the case that doesn't work though.)
Before I found ReplaceFileA() I tried doing _unlink() before posixy_rename() which fails in the same way under Wine but seems to work on the real platform. It would be problematic to do this as it means the replacement isn't atomic as there's a window when the target is missing, but I thought it interesting to try.
I think this is much improved yet still not perfect. The only obvious thing I can see we could do is rename to a random hidden name before deletion, but it seems that's not needed on modern OS versions (at least on NTFS).
I'll add a comment noting that to the source code, but it's such a fight to get the behaviour we want on this platform that I don't think I'm going to spend more time on this unless/until we get feedback from people actually using Xapian on Microsoft Windows and actually hitting problems.

hmm, but if we do this, what happens on windows if the file is overwritten while we're reading it? The read will fail I believe...