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