|
| 1 | +using J2N; |
1 | 2 | using Lucene.Net.Attributes; |
2 | 3 | using Lucene.Net.Documents; |
3 | 4 | using Lucene.Net.Index.Extensions; |
@@ -1066,6 +1067,268 @@ public void TestDisposingCloneDoesNotAffectRootOrSiblings() |
1066 | 1067 | cloneB.Dispose(); |
1067 | 1068 | } |
1068 | 1069 |
|
| 1070 | + // Multiple slices over the same file must each see their own region |
| 1071 | + // of data, not stale bytes from a neighboring slice. Each OpenSlice |
| 1072 | + // in the new design opens its own MemoryMappedFile+view over the |
| 1073 | + // given (offset, length) window, so bounds are enforced per-slice |
| 1074 | + // and reads are independent. |
| 1075 | + [Test, LuceneNetSpecific] |
| 1076 | + public void TestMultipleSlicesReadDistinctData() |
| 1077 | + { |
| 1078 | + var dirPath = CreateTempDir("testMultipleSlicesDistinct"); |
| 1079 | + using var mmapDir = new MMapDirectory(dirPath); |
| 1080 | + const string name = "bytes"; |
| 1081 | + const int regionSize = 4096; |
| 1082 | + const int regions = 4; |
| 1083 | + |
| 1084 | + // Write four contiguous 4KiB regions, each filled with a distinct |
| 1085 | + // byte pattern (0x11, 0x22, 0x33, 0x44). Later we open a slice |
| 1086 | + // over each region and check that reads return the right pattern. |
| 1087 | + using (var io = mmapDir.CreateOutput(name, NewIOContext(Random))) |
| 1088 | + { |
| 1089 | + var buf = new byte[regionSize]; |
| 1090 | + for (int r = 0; r < regions; r++) |
| 1091 | + { |
| 1092 | + byte fill = (byte)(0x11 * (r + 1)); |
| 1093 | + for (int i = 0; i < buf.Length; i++) buf[i] = fill; |
| 1094 | + io.WriteBytes(buf, 0, buf.Length); |
| 1095 | + } |
| 1096 | + } |
| 1097 | + |
| 1098 | + using var slicer = mmapDir.CreateSlicer(name, NewIOContext(Random)); |
| 1099 | + |
| 1100 | + var slices = new IndexInput[regions]; |
| 1101 | + try |
| 1102 | + { |
| 1103 | + for (int r = 0; r < regions; r++) |
| 1104 | + { |
| 1105 | + slices[r] = slicer.OpenSlice("slice" + r, r * regionSize, regionSize); |
| 1106 | + Assert.AreEqual(regionSize, slices[r].Length, $"slice {r} length"); |
| 1107 | + } |
| 1108 | + |
| 1109 | + // Each slice sees only its own pattern. |
| 1110 | + for (int r = 0; r < regions; r++) |
| 1111 | + { |
| 1112 | + byte expected = (byte)(0x11 * (r + 1)); |
| 1113 | + for (int i = 0; i < regionSize; i++) |
| 1114 | + { |
| 1115 | + byte b = slices[r].ReadByte(); |
| 1116 | + if (b != expected) |
| 1117 | + { |
| 1118 | + Assert.Fail( |
| 1119 | + $"slice {r} at offset {i}: expected 0x{expected:X2}, got 0x{b:X2}"); |
| 1120 | + } |
| 1121 | + } |
| 1122 | + |
| 1123 | + // Reading past the slice's length must fail with EOF. |
| 1124 | + try |
| 1125 | + { |
| 1126 | + slices[r].ReadByte(); |
| 1127 | + Assert.Fail($"slice {r}: read past end of slice must throw EOF"); |
| 1128 | + } |
| 1129 | + catch (EndOfStreamException) |
| 1130 | + { |
| 1131 | + // expected |
| 1132 | + } |
| 1133 | + } |
| 1134 | + |
| 1135 | + // Concurrent reads across sibling slices: each worker scans |
| 1136 | + // its own slice fully and asserts the pattern. If the slices |
| 1137 | + // were accidentally aliased to the same underlying view, |
| 1138 | + // racing Seek() calls would cross-contaminate. |
| 1139 | + var errors = new System.Collections.Concurrent.ConcurrentBag<string>(); |
| 1140 | + var tasks = new System.Threading.Tasks.Task[regions]; |
| 1141 | + for (int r = 0; r < regions; r++) |
| 1142 | + { |
| 1143 | + int idx = r; |
| 1144 | + byte expected = (byte)(0x11 * (idx + 1)); |
| 1145 | + var clone = (IndexInput)slices[idx].Clone(); |
| 1146 | + tasks[idx] = System.Threading.Tasks.Task.Run(() => |
| 1147 | + { |
| 1148 | + for (int pass = 0; pass < 50; pass++) |
| 1149 | + { |
| 1150 | + clone.Seek(0); |
| 1151 | + for (int i = 0; i < regionSize; i++) |
| 1152 | + { |
| 1153 | + byte b = clone.ReadByte(); |
| 1154 | + if (b != expected) |
| 1155 | + { |
| 1156 | + errors.Add($"slice {idx} pass {pass} offset {i}: expected 0x{expected:X2}, got 0x{b:X2}"); |
| 1157 | + return; |
| 1158 | + } |
| 1159 | + } |
| 1160 | + } |
| 1161 | + }); |
| 1162 | + } |
| 1163 | + System.Threading.Tasks.Task.WaitAll(tasks); |
| 1164 | + if (!errors.IsEmpty) |
| 1165 | + { |
| 1166 | + Assert.Fail("Cross-slice contamination detected:\n" + string.Join("\n", errors)); |
| 1167 | + } |
| 1168 | + } |
| 1169 | + finally |
| 1170 | + { |
| 1171 | + foreach (var s in slices) s?.Dispose(); |
| 1172 | + } |
| 1173 | + } |
| 1174 | + |
| 1175 | + // Disposing a single slice must not affect its sibling slices from |
| 1176 | + // the same slicer. In the new design each OpenSlice has its own |
| 1177 | + // View, so slice.Dispose closes that slice's view only. Slicer |
| 1178 | + // cascade Dispose is covered by TestCloneSliceSafety. |
| 1179 | + [Test, LuceneNetSpecific] |
| 1180 | + public void TestDisposingOneSliceDoesNotAffectSiblings() |
| 1181 | + { |
| 1182 | + var dirPath = CreateTempDir("testSliceSiblingIsolation"); |
| 1183 | + using var mmapDir = new MMapDirectory(dirPath); |
| 1184 | + const string name = "bytes"; |
| 1185 | + using (var io = mmapDir.CreateOutput(name, NewIOContext(Random))) |
| 1186 | + { |
| 1187 | + for (int i = 0; i < 8; i++) io.WriteInt32(i); |
| 1188 | + } |
| 1189 | + |
| 1190 | + using var slicer = mmapDir.CreateSlicer(name, NewIOContext(Random)); |
| 1191 | + var sliceA = slicer.OpenSlice("sliceA", 0, 16); |
| 1192 | + var sliceB = slicer.OpenSlice("sliceB", 16, 16); |
| 1193 | + |
| 1194 | + sliceA.Dispose(); |
| 1195 | + |
| 1196 | + // Sibling must continue to work and return its own data. |
| 1197 | + Assert.AreEqual(4, sliceB.ReadInt32()); |
| 1198 | + Assert.AreEqual(5, sliceB.ReadInt32()); |
| 1199 | + |
| 1200 | + // Disposed slice must throw on read. |
| 1201 | + try |
| 1202 | + { |
| 1203 | + sliceA.ReadInt32(); |
| 1204 | + Assert.Fail("Disposed slice must throw AlreadyClosed"); |
| 1205 | + } |
| 1206 | + catch (Exception e) when (e.IsAlreadyClosedException()) |
| 1207 | + { |
| 1208 | + // expected |
| 1209 | + } |
| 1210 | + |
| 1211 | + sliceB.Dispose(); |
| 1212 | + } |
| 1213 | + |
| 1214 | + // Open a 3.0 CFS index with MMapDirectory and read it end-to-end |
| 1215 | + // through DirectoryReader. 3.0 (pre-3.1) CFS files are the only |
| 1216 | + // remaining caller of IndexInputSlicer.OpenFullSlice, which in our |
| 1217 | + // new slicer requires a disposed-state check before it can trust |
| 1218 | + // descriptor.Length. If that path is broken, DirectoryReader.Open |
| 1219 | + // will throw while reading segment headers. |
| 1220 | + [Test, LuceneNetSpecific] |
| 1221 | + public void TestRead3xCfsIndex_ViaMMap() |
| 1222 | + { |
| 1223 | + var indexDir = CreateTempDir("test3xCfsIndex"); |
| 1224 | + using (var zip = GetType().FindAndGetManifestResourceStream("index.30.cfs.zip")) |
| 1225 | + { |
| 1226 | + Assert.IsNotNull(zip, "expected index.30.cfs.zip to be embedded"); |
| 1227 | + TestUtil.Unzip(zip, indexDir); |
| 1228 | + } |
| 1229 | + |
| 1230 | + using var mmapDir = new MMapDirectory(indexDir); |
| 1231 | + using var reader = Index.DirectoryReader.Open(mmapDir); |
| 1232 | + |
| 1233 | + Assert.IsTrue(reader.MaxDoc > 0, "3.0 index should contain documents"); |
| 1234 | + |
| 1235 | + // Touch each leaf to force real reads through the CFS + |
| 1236 | + // OpenFullSlice path. This will throw AVE on a broken mmap |
| 1237 | + // teardown or on a bad OpenFullSlice. |
| 1238 | + int totalDocs = 0; |
| 1239 | + foreach (var leaf in reader.Leaves) |
| 1240 | + { |
| 1241 | + var atomic = leaf.AtomicReader; |
| 1242 | + for (int i = 0; i < atomic.MaxDoc; i++) |
| 1243 | + { |
| 1244 | + if (atomic.LiveDocs != null && !atomic.LiveDocs.Get(i)) |
| 1245 | + continue; |
| 1246 | + var doc = atomic.Document(i); |
| 1247 | + Assert.IsNotNull(doc, $"doc {i} in leaf {leaf} should not be null"); |
| 1248 | + totalDocs++; |
| 1249 | + } |
| 1250 | + } |
| 1251 | + Assert.IsTrue(totalDocs > 0, "at least one live 3.0 document should be readable"); |
| 1252 | + } |
| 1253 | + |
| 1254 | + // Directly exercise the OpenFullSlice entry point on a 3.0 .cfs |
| 1255 | + // file. OpenFullSlice is [Obsolete("Only for reading CFS files |
| 1256 | + // from 3.x indexes.")] — the test pins that it still produces an |
| 1257 | + // IndexInput spanning the whole file and that its bytes match the |
| 1258 | + // plain OpenInput read of the same file. |
| 1259 | + [Test, LuceneNetSpecific] |
| 1260 | + public void TestOpenFullSlice_On3xCfsFile_MatchesOpenInput() |
| 1261 | + { |
| 1262 | + var indexDir = CreateTempDir("test3xOpenFullSlice"); |
| 1263 | + using (var zip = GetType().FindAndGetManifestResourceStream("index.30.cfs.zip")) |
| 1264 | + { |
| 1265 | + Assert.IsNotNull(zip, "expected index.30.cfs.zip to be embedded"); |
| 1266 | + TestUtil.Unzip(zip, indexDir); |
| 1267 | + } |
| 1268 | + |
| 1269 | + // Pick the first .cfs file in the unzipped 3.0 index. |
| 1270 | + string cfsName = null; |
| 1271 | + foreach (var f in indexDir.GetFiles("*.cfs")) |
| 1272 | + { |
| 1273 | + cfsName = f.Name; |
| 1274 | + break; |
| 1275 | + } |
| 1276 | + Assert.IsNotNull(cfsName, "expected at least one .cfs file in the 3.0 index"); |
| 1277 | + |
| 1278 | + using var mmapDir = new MMapDirectory(indexDir); |
| 1279 | + |
| 1280 | + byte[] viaOpenInput; |
| 1281 | + using (var input = mmapDir.OpenInput(cfsName, NewIOContext(Random))) |
| 1282 | + { |
| 1283 | + viaOpenInput = new byte[input.Length]; |
| 1284 | + input.ReadBytes(viaOpenInput, 0, viaOpenInput.Length); |
| 1285 | + } |
| 1286 | + |
| 1287 | + byte[] viaFullSlice; |
| 1288 | + using (var slicer = mmapDir.CreateSlicer(cfsName, NewIOContext(Random))) |
| 1289 | + { |
| 1290 | +#pragma warning disable 612, 618 |
| 1291 | + using var full = slicer.OpenFullSlice(); |
| 1292 | +#pragma warning restore 612, 618 |
| 1293 | + Assert.AreEqual(viaOpenInput.Length, full.Length, |
| 1294 | + "OpenFullSlice length must match OpenInput length"); |
| 1295 | + viaFullSlice = new byte[full.Length]; |
| 1296 | + full.ReadBytes(viaFullSlice, 0, viaFullSlice.Length); |
| 1297 | + } |
| 1298 | + |
| 1299 | + Assert.AreEqual(viaOpenInput, viaFullSlice, |
| 1300 | + "OpenFullSlice bytes must match OpenInput bytes for the same CFS file"); |
| 1301 | + } |
| 1302 | + |
| 1303 | + // OpenFullSlice on a slicer that has been disposed must throw |
| 1304 | + // AlreadyClosedException, not ObjectDisposedException leaking from |
| 1305 | + // the underlying FileStream. Part of the review-item-6 fix. |
| 1306 | + [Test, LuceneNetSpecific] |
| 1307 | + public void TestOpenFullSlice_AfterDispose_ThrowsAlreadyClosed() |
| 1308 | + { |
| 1309 | + var dirPath = CreateTempDir("testFullSliceAfterDispose"); |
| 1310 | + using var mmapDir = new MMapDirectory(dirPath); |
| 1311 | + using (var io = mmapDir.CreateOutput("bytes", NewIOContext(Random))) |
| 1312 | + { |
| 1313 | + io.WriteInt32(42); |
| 1314 | + } |
| 1315 | + |
| 1316 | + var slicer = mmapDir.CreateSlicer("bytes", NewIOContext(Random)); |
| 1317 | + slicer.Dispose(); |
| 1318 | + |
| 1319 | + try |
| 1320 | + { |
| 1321 | +#pragma warning disable 612, 618 |
| 1322 | + slicer.OpenFullSlice(); |
| 1323 | +#pragma warning restore 612, 618 |
| 1324 | + Assert.Fail("OpenFullSlice on disposed slicer must throw AlreadyClosed"); |
| 1325 | + } |
| 1326 | + catch (Exception e) when (e.IsAlreadyClosedException()) |
| 1327 | + { |
| 1328 | + // expected |
| 1329 | + } |
| 1330 | + } |
| 1331 | + |
1069 | 1332 | [Test, LuceneNetSpecific] |
1070 | 1333 | public void TestDisposeIndexInput() |
1071 | 1334 | { |
|
0 commit comments