Skip to content

Commit c9f38ce

Browse files
paulirwinclaude
andcommitted
Open the file with FileShare.ReadWrite | Delete on Windows for delete-while-mapped
The path-based MemoryMappedFile.CreateFromFile overload internally opens the file with FileShare.Read. On Windows that blocks any subsequent open-for-write or open-for-delete on the same file while we hold the mapping. Callers like FreeTextSuggester build a temp index, dispose the directory, and then recursively delete the directory; on Windows that recursive delete fails with "The process cannot access the file ... because it is being used by another process" because Windows requires FILE_SHARE_DELETE on the existing handle for a delete to proceed against an open file. Switch back to opening our own FileStream so we control the share flags. We use FileShare.ReadWrite | FileShare.Delete to match the prior behavior (other writers/deleters can proceed; Windows will defer the actual unlink until our last close, which is the standard Unix-like semantic the rest of the framework expects). We still pass capacity: 0 to CreateFromFile so the framework does its own size stat — the #1090 race window stays closed. leaveOpen: false hands the FileStream's lifetime to the MMF, so we don't have to track it as a SharedMapping field. Zero-length files are handled up front rather than letting CreateViewAccessor reject the empty view: we dispose the FileStream eagerly and return an empty SharedMapping. Caught by Windows CI: TestFreeTextSuggester.TestBasic and siblings failed with InvalidOperationException("failed to remove ...") wrapped around IOException("write.lock ... being used by another process") on net8.0 / net472 / net48 Windows runners. Linux passed because Linux's "delete while open" semantics don't depend on the open handle's share mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 899ec1f commit c9f38ce

1 file changed

Lines changed: 47 additions & 21 deletions

File tree

src/Lucene.Net/Store/MMapDirectory.cs

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -765,44 +765,70 @@ private SharedMapping(MemoryMappedFile? mmf, Chunk[] chunks, long length)
765765

766766
internal static SharedMapping Create(string file, int chunkSizePower)
767767
{
768-
// We don't track a separate FileStream: the path-based
769-
// CreateFromFile overload opens its own handle and
770-
// disposes it with the MemoryMappedFile. We capture the
771-
// file length once via FileInfo for our own snapshot
772-
// (used as the slice/range upper bound). Any divergence
773-
// between this snapshot and the framework's internal
774-
// stat — e.g. the file growing in between, formerly the
775-
// #1090 race — is harmless: the mmap itself is sized by
776-
// the framework's own stat, and our `length` is treated
777-
// as a snapshot at open time (matching upstream Java's
778-
// fc.size() snapshot semantics).
779-
long length = new FileInfo(file).Length;
780-
if (length == 0)
781-
{
782-
return new SharedMapping(mmf: null, chunks: Array.Empty<Chunk>(), length: 0);
783-
}
784-
768+
// We open our own FileStream so we control the FileShare
769+
// flags. The path-based CreateFromFile overload internally
770+
// opens with FileShare.Read, which on Windows blocks
771+
// attempts to delete or write to this file while we have
772+
// it mapped — breaking callers (e.g. FreeTextSuggester)
773+
// that build a temp index, dispose the directory, and
774+
// immediately recursively delete the directory. We need
775+
// FileShare.Delete in particular: on Windows, a delete
776+
// attempt against an open file fails unless the open
777+
// share-mode permits FILE_SHARE_DELETE.
778+
//
779+
// bufferSize: 1 because MemoryMappedFile uses only the
780+
// file handle and bypasses the FileStream buffer, so a
781+
// 4 KiB default buffer would just be allocated and
782+
// immediately discarded.
783+
FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read,
784+
FileShare.ReadWrite | FileShare.Delete,
785+
bufferSize: 1, FileOptions.RandomAccess);
785786
MemoryMappedFile? mmf = null;
786787
Chunk[]? chunks = null;
787788
try
788789
{
790+
long length = fs.Length;
791+
if (length == 0)
792+
{
793+
// CreateViewAccessor rejects zero-length views and
794+
// CreateFromFile rejects capacity 0 on an empty file,
795+
// so handle this edge case ourselves. Dispose the
796+
// FileStream eagerly since there's no MMF to own it.
797+
fs.Dispose();
798+
return new SharedMapping(mmf: null, chunks: Array.Empty<Chunk>(), length: 0);
799+
}
800+
789801
// capacity: 0 -> the framework uses the file's
790802
// current size on disk, atomically with mapping
791803
// creation. This eliminates the #1090 race window
792804
// we previously had to retry around.
805+
// leaveOpen: false -> the MMF takes ownership of the
806+
// FileStream and disposes it on its own Dispose, so
807+
// we don't need to track it ourselves.
793808
mmf = MemoryMappedFile.CreateFromFile(
794-
path: file,
795-
mode: FileMode.Open,
809+
fileStream: fs,
796810
mapName: null,
797811
capacity: 0,
798-
access: MemoryMappedFileAccess.Read);
812+
access: MemoryMappedFileAccess.Read,
813+
#if FEATURE_MEMORYMAPPEDFILESECURITY
814+
memoryMappedFileSecurity: null,
815+
#endif
816+
inheritability: HandleInheritability.None,
817+
leaveOpen: false);
799818
chunks = MapChunks(mmf, 0, length, chunkSizePower);
800819
return new SharedMapping(mmf, chunks, length);
801820
}
802821
catch
803822
{
804823
DisposeChunks(chunks);
805-
mmf?.Dispose();
824+
if (mmf != null)
825+
{
826+
mmf.Dispose(); // disposes fs (leaveOpen: false)
827+
}
828+
else
829+
{
830+
IOUtils.DisposeWhileHandlingException(fs);
831+
}
806832
throw;
807833
}
808834
}

0 commit comments

Comments
 (0)