@@ -194,31 +194,24 @@ public override IndexInput OpenInput(string name, IOContext context)
194194 // Matches upstream Java (openInput creates a new FileChannel +
195195 // fc.map()) and ensures Length reflects the file's current size.
196196 SharedMapping mapping = SharedMapping . Create ( file , chunkSizePower ) ;
197- try
198- {
199- return new MMapIndexInput ( $ "MMapIndexInput(path=\" { file } \" )", ownsMapping : true , mapping , 0 , mapping . Length , chunkSizePower ) ;
200- }
201- catch
202- {
203- mapping . Dispose ( ) ;
204- throw ;
205- }
197+ // Ownership of the mapping transfers to the returned root
198+ // MMapIndexInput (ownsMapping: true); the caller of OpenInput is
199+ // responsible for disposing that input, which disposes the mapping.
200+ // The constructor only sets fields and cannot throw here, so no
201+ // catch/dispose guard is needed.
202+ return new MMapIndexInput ( $ "MMapIndexInput(path=\" { file } \" )", ownsMapping : true , mapping , 0 , mapping . Length , chunkSizePower ) ;
206203 }
207204
208205 public override IndexInputSlicer CreateSlicer ( string name , IOContext context )
209206 {
210207 EnsureOpen ( ) ;
211208 var file = Path . Combine ( Directory . FullName , name ) ;
212209 SharedMapping mapping = SharedMapping . Create ( file , chunkSizePower ) ;
213- try
214- {
215- return new IndexInputSlicerAnonymousClass ( this , file , mapping ) ;
216- }
217- catch
218- {
219- mapping . Dispose ( ) ;
220- throw ;
221- }
210+ // Ownership of the mapping transfers to the returned slicer; the
211+ // caller of CreateSlicer is responsible for disposing that slicer,
212+ // which disposes the mapping. The constructor only sets fields and
213+ // cannot throw here, so no catch/dispose guard is needed.
214+ return new IndexInputSlicerAnonymousClass ( this , file , mapping ) ;
222215 }
223216
224217 private sealed class IndexInputSlicerAnonymousClass : IndexInputSlicer
@@ -246,6 +239,15 @@ public IndexInputSlicerAnonymousClass(MMapDirectory outerInstance, string file,
246239 this . mapping = mapping ;
247240 }
248241
242+ // Returns a slice the CALLER must dispose. The slice does not own the
243+ // mapping (ownsMapping: false); it is also tracked in issuedSlices so
244+ // that disposing the slicer cascades to any slices the caller left
245+ // open. Note a slice can outlive a Dispose of outerInstance (the
246+ // MMapDirectory): we deliberately do not thread outerInstance into the
247+ // slice to re-check on every read, because reads against a disposed
248+ // mapping already fail fast with AlreadyClosedException via the
249+ // mapping's closed flag and per-chunk rent. EnsureOpen here only guards
250+ // the act of opening a new slice.
249251 public override IndexInput OpenSlice ( string sliceDescription , long offset , long length )
250252 {
251253 outerInstance . EnsureOpen ( ) ;
@@ -279,8 +281,13 @@ public override IndexInput OpenSlice(string sliceDescription, long offset, long
279281 public override IndexInput OpenFullSlice ( )
280282 {
281283 outerInstance . EnsureOpen ( ) ;
282- // The shared mapping's Length was captured at creation time,
283- // so we don't need to touch any FileStream here.
284+ // A full slice is just a slice over the whole mapping. It shares
285+ // the slicer's single SharedMapping (same MemoryMappedFile and
286+ // FileStream) rather than opening a second mapping, and it is
287+ // tracked in issuedSlices like any other slice, so disposing the
288+ // slicer disposes it and the one backing FileStream. The mapping's
289+ // Length was captured at creation time, so we touch no FileStream
290+ // here.
284291 return OpenSlice ( "full-slice" , 0 , mapping . Length ) ;
285292 }
286293
@@ -398,6 +405,11 @@ internal sealed unsafe class MMapIndexInput : IndexInput
398405 // on Dispose. Slices and clones do not own it.
399406 private readonly SharedMapping mapping ;
400407
408+ // LUCENENET specific (PR #1267): for testing only. Exposes the shared
409+ // mapping so a test can assert that disposing a root input
410+ // deterministically disposes the mapping's backing FileStream.
411+ internal SharedMapping Mapping => mapping ;
412+
401413 // The window into the shared mapping that this IndexInput sees.
402414 // For OpenInput this is [0, mapping.Length); for OpenSlice it is
403415 // the requested slice range. All offsets in the cached chunk
@@ -830,11 +842,34 @@ internal sealed unsafe class SharedMapping : IDisposable
830842 /// Note that this can be null in the edge case of a zero-length mapping.
831843 /// </summary>
832844 private readonly MemoryMappedFile ? memoryMappedFile ;
845+
846+ /// <summary>
847+ /// The <see cref="FileStream"/> backing <see cref="memoryMappedFile"/>.
848+ /// We pass this stream to
849+ /// <see cref="MemoryMappedFile.CreateFromFile(FileStream, string?, long, MemoryMappedFileAccess, HandleInheritability, bool)"/>
850+ /// with <c>leaveOpen: true</c>, so the mapping borrows the file handle
851+ /// but never disposes the <see cref="FileStream"/> object. This mapping
852+ /// owns it and disposes it in <see cref="Dispose"/> so the stream (a
853+ /// finalizable object holding the file handle) is released
854+ /// deterministically rather than left to the finalizer. Null for the
855+ /// zero-length edge case (no mapping is created).
856+ /// </summary>
857+ private readonly FileStream ? fileStream ;
833858 private int disposed ;
834859
835- private SharedMapping ( MemoryMappedFile ? mmf , Chunk [ ] chunks , long length )
860+ // LUCENENET specific (PR #1267): for testing only. True once Dispose has
861+ // run and the owned FileStream (if any) has been disposed. Lets a test
862+ // assert that the mapping releases its FileStream deterministically
863+ // instead of leaking the object to finalization. Always true for the
864+ // zero-length edge case, which owns no FileStream.
865+ internal bool IsFileStreamDisposed =>
866+ Volatile . Read ( ref disposed ) != 0 &&
867+ ( fileStream is null || ! fileStream . CanRead ) ;
868+
869+ private SharedMapping ( MemoryMappedFile ? mmf , FileStream ? fileStream , Chunk [ ] chunks , long length )
836870 {
837871 this . memoryMappedFile = mmf ;
872+ this . fileStream = fileStream ;
838873 this . Chunks = chunks ;
839874 this . Length = length ;
840875 }
@@ -910,7 +945,6 @@ private static SharedMapping CreateAttempt(string file, int chunkSizePower)
910945 bufferSize : 1 , FileOptions . RandomAccess ) ;
911946 MemoryMappedFile ? mmf = null ;
912947 Chunk [ ] ? chunks = null ;
913- Exception ? priorException = null ;
914948 try
915949 {
916950 long length = fs . Length ;
@@ -925,7 +959,7 @@ private static SharedMapping CreateAttempt(string file, int chunkSizePower)
925959 // (it would be misleading — we successfully built a
926960 // zero-length mapping).
927961 IOUtils . DisposeWhileHandlingException ( fs ) ;
928- return new SharedMapping ( mmf : null , chunks : Array . Empty < Chunk > ( ) , length : 0 ) ;
962+ return new SharedMapping ( mmf : null , fileStream : null , chunks : Array . Empty < Chunk > ( ) , length : 0 ) ;
929963 }
930964
931965 // capacity: 0 -> the framework sizes the mapping
@@ -935,9 +969,14 @@ private static SharedMapping CreateAttempt(string file, int chunkSizePower)
935969 // the length is re-read across the defaulting and
936970 // validation steps, which is why Create wraps this
937971 // call in a retry loop.
938- // leaveOpen: false -> the MMF takes ownership of
939- // the FileStream and disposes it on its own
940- // Dispose, so we don't need to track it ourselves.
972+ // leaveOpen: true -> the MMF borrows fs's file handle
973+ // but does not close it; SharedMapping owns the
974+ // FileStream and disposes it (which closes the handle)
975+ // in Dispose. Note that even with leaveOpen: false the
976+ // MMF would close only the handle, never the FileStream
977+ // object itself, so we must track fs either way; using
978+ // leaveOpen: true keeps a single, unambiguous owner of
979+ // the handle and avoids a redundant handle close.
941980 mmf = MemoryMappedFile . CreateFromFile (
942981 fileStream : fs ,
943982 mapName : null ,
@@ -947,30 +986,28 @@ private static SharedMapping CreateAttempt(string file, int chunkSizePower)
947986 memoryMappedFileSecurity : null ,
948987#endif
949988 inheritability: HandleInheritability. None ,
950- leaveOpen : false ) ;
989+ leaveOpen : true ) ;
951990 chunks = MapChunks ( mmf , 0 , length , chunkSizePower ) ;
952- return new SharedMapping ( mmf , chunks , length ) ;
991+ return new SharedMapping ( mmf , fs , chunks , length ) ;
953992 }
954993 catch ( Exception e ) when ( e . IsThrowable ( ) )
955994 {
956- priorException = e ;
995+ // Cleanup must not mask e. DisposeChunks swallows internally,
996+ // so chunk teardown is safe. We dispose mmf/fs through the
997+ // swallowing overload of DisposeWhileHandlingException (the one
998+ // with no Exception parameter), which suppresses any Dispose
999+ // failure, and then rethrow e with a bare `throw;`. A bare
1000+ // rethrow preserves e's original stack trace, and using the
1001+ // swallowing overload (rather than the priorException overload,
1002+ // which would ALSO throw) avoids a confusing double-throw.
1003+ // With leaveOpen: true we always own fs (the MMF never disposes
1004+ // it), so dispose both the mmf (if it was created) and fs. mmf
1005+ // first so the mapping is torn down before the backing handle
1006+ // is closed.
1007+ DisposeChunks ( chunks ) ;
1008+ IOUtils . DisposeWhileHandlingException ( mmf , fs ) ;
9571009 throw ;
9581010 }
959- finally
960- {
961- if ( priorException != null )
962- {
963- // Cleanup must not mask priorException. DisposeChunks
964- // swallows internally, so chunk teardown is safe.
965- // For the mmf/fs the priorException overload attaches
966- // any Dispose failure as a suppressed exception and
967- // rethrows the original.
968- // mmf owns fs once CreateFromFile returned (leaveOpen:
969- // false); if mmf is null, fs ownership is still ours.
970- DisposeChunks ( chunks ) ;
971- IOUtils . DisposeWhileHandlingException ( priorException , ( IDisposable ? ) mmf ?? fs ) ;
972- }
973- }
9741011 }
9751012
9761013 /// <summary>
@@ -980,7 +1017,11 @@ public void Dispose()
9801017 {
9811018 if ( Interlocked . CompareExchange ( ref disposed , 1 , 0 ) != 0 ) return ;
9821019 DisposeChunks ( Chunks ) ;
983- IOUtils . DisposeWhileHandlingException ( memoryMappedFile ) ;
1020+ // Tear down the mapping before closing the backing handle, then
1021+ // dispose the FileStream we own (the MMF was created with
1022+ // leaveOpen: true and never disposes it). fileStream is null for
1023+ // the zero-length edge case, which the overload tolerates.
1024+ IOUtils . DisposeWhileHandlingException ( memoryMappedFile , fileStream ) ;
9841025 }
9851026
9861027 internal Chunk [ ] Chunks { get ; }
@@ -1019,27 +1060,22 @@ private static Chunk[] MapChunks(MemoryMappedFile? mmf, long offset, long length
10191060
10201061 MemoryMappedViewAccessor accessor = mmf . CreateViewAccessor ( chunkOffset , thisChunkLen , MemoryMappedFileAccess . Read ) ;
10211062 byte * ptr = null ;
1022- Exception ? acquireException = null ;
10231063 try
10241064 {
10251065 accessor . SafeMemoryMappedViewHandle . AcquirePointer ( ref ptr ) ;
10261066 }
10271067 catch ( Exception e ) when ( e . IsThrowable ( ) )
10281068 {
10291069 // Don't let accessor.Dispose() mask the original
1030- // AcquirePointer failure — route through the
1031- // priorException overload so any Dispose throw
1032- // becomes a suppressed exception on the original.
1033- acquireException = e ;
1070+ // AcquirePointer failure: dispose the accessor through the
1071+ // swallowing overload (suppressing any Dispose failure),
1072+ // then rethrow e with a bare `throw;` that preserves its
1073+ // original stack trace. The throw skips the Chunk
1074+ // construction below and propagates to the outer catch,
1075+ // which disposes the already-built chunks.
1076+ IOUtils . DisposeWhileHandlingException ( accessor ) ;
10341077 throw ;
10351078 }
1036- finally
1037- {
1038- if ( acquireException != null )
1039- {
1040- IOUtils . DisposeWhileHandlingException ( acquireException , accessor ) ;
1041- }
1042- }
10431079 // The accessor may be mapped at an offset inside the OS page,
10441080 // in which case PointerOffset is the distance from the
10451081 // SafeBuffer's base to the first byte of the requested view.
0 commit comments