|
28 | 28 | import java.util.concurrent.ExecutorService; |
29 | 29 | import java.util.concurrent.Executors; |
30 | 30 | import java.util.concurrent.Future; |
| 31 | +import java.util.concurrent.TimeUnit; |
31 | 32 |
|
32 | 33 | import org.junit.jupiter.api.Test; |
33 | 34 | import org.junit.jupiter.api.io.TempDir; |
@@ -77,6 +78,13 @@ public class SharedServerModeTest { |
77 | 78 | "<fakeload millis=\"60000\" cpu=\"1\" mb=\"10\"/>" + |
78 | 79 | "</mock>"; |
79 | 80 |
|
| 81 | + private static final String MOCK_INFLIGHT = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" + |
| 82 | + "<mock>" + |
| 83 | + "<metadata action=\"add\" name=\"dc:creator\">In-flight Author</metadata>" + |
| 84 | + "<write element=\"p\">In-flight content</write>" + |
| 85 | + "<fakeload millis=\"3000\" cpu=\"1\" mb=\"10\"/>" + |
| 86 | + "</mock>"; |
| 87 | + |
80 | 88 | @Test |
81 | 89 | public void testBasicSharedMode(@TempDir Path tmp) throws Exception { |
82 | 90 | Path inputDir = setupInputDir(tmp); |
@@ -216,6 +224,78 @@ public void testGracefulShutdown(@TempDir Path tmp) throws Exception { |
216 | 224 | } |
217 | 225 | } |
218 | 226 |
|
| 227 | + /** |
| 228 | + * Verifies that close() unblocks an in-flight parse and returns promptly when |
| 229 | + * the parser is shut down at a randomized point in the parse lifecycle. |
| 230 | + * <p> |
| 231 | + * After a warmup parse to ensure the shared server is fully started, this picks |
| 232 | + * a random sleep in [0, 4000]ms before calling close(). Given the inflight |
| 233 | + * fakeload runs ~3000ms, the chosen delay can land in init, mid-parse, near the |
| 234 | + * tail of parse, or post-completion. The seed is logged and included in every |
| 235 | + * assertion message so a CI failure can be reproduced via |
| 236 | + * {@code -DcloseInFlightSeed=<seed>}. |
| 237 | + */ |
| 238 | + @Test |
| 239 | + public void testCloseAtRandomPhase(@TempDir Path tmp) throws Exception { |
| 240 | + long seed = Long.getLong("closeInFlightSeed", System.nanoTime()); |
| 241 | + java.util.Random rng = new java.util.Random(seed); |
| 242 | + long sleepBeforeCloseMs = rng.nextInt(4001); // [0, 4000] |
| 243 | + String repro = "[seed=" + seed + ", sleep=" + sleepBeforeCloseMs + "ms]"; |
| 244 | + System.out.println("testCloseAtRandomPhase " + repro); |
| 245 | + |
| 246 | + Path inputDir = setupInputDir(tmp); |
| 247 | + Files.writeString(inputDir.resolve("warmup.xml"), MOCK_OK, StandardCharsets.UTF_8); |
| 248 | + Files.writeString(inputDir.resolve("inflight.xml"), MOCK_INFLIGHT, StandardCharsets.UTF_8); |
| 249 | + |
| 250 | + Path tikaConfigPath = PluginsTestHelper.getFileSystemFetcherConfig( |
| 251 | + "tika-config-shared-server.json", tmp, inputDir, tmp.resolve("output"), false); |
| 252 | + TikaJsonConfig tikaJsonConfig = TikaJsonConfig.load(tikaConfigPath); |
| 253 | + PipesConfig pipesConfig = PipesConfig.load(tikaJsonConfig); |
| 254 | + |
| 255 | + PipesParser pipesParser = PipesParser.load(tikaJsonConfig, pipesConfig, tikaConfigPath); |
| 256 | + ExecutorService executor = Executors.newSingleThreadExecutor(); |
| 257 | + try { |
| 258 | + // Warmup so the shared server is fully started; without this, short |
| 259 | + // sleeps would always land in init rather than reaching mid-parse. |
| 260 | + PipesResult warmup = pipesParser.parse(new FetchEmitTuple( |
| 261 | + "warmup.xml", |
| 262 | + new FetchKey(FETCHER_NAME, "warmup.xml"), |
| 263 | + new EmitKey(EMITTER_NAME, ""), |
| 264 | + new Metadata(), |
| 265 | + new ParseContext(), |
| 266 | + FetchEmitTuple.ON_PARSE_EXCEPTION.SKIP)); |
| 267 | + assertTrue(warmup.isSuccess(), "Warmup parse must succeed " + repro); |
| 268 | + |
| 269 | + Future<PipesResult> future = executor.submit(() -> pipesParser.parse(new FetchEmitTuple( |
| 270 | + "inflight.xml", |
| 271 | + new FetchKey(FETCHER_NAME, "inflight.xml"), |
| 272 | + new EmitKey(EMITTER_NAME, ""), |
| 273 | + new Metadata(), |
| 274 | + new ParseContext(), |
| 275 | + FetchEmitTuple.ON_PARSE_EXCEPTION.SKIP))); |
| 276 | + |
| 277 | + if (sleepBeforeCloseMs > 0) { |
| 278 | + Thread.sleep(sleepBeforeCloseMs); |
| 279 | + } |
| 280 | + |
| 281 | + long closeStart = System.currentTimeMillis(); |
| 282 | + pipesParser.close(); |
| 283 | + long closeDuration = System.currentTimeMillis() - closeStart; |
| 284 | + |
| 285 | + assertTrue(closeDuration < 15000, |
| 286 | + "close() should return promptly even with in-flight work; took " + |
| 287 | + closeDuration + "ms " + repro); |
| 288 | + |
| 289 | + // The in-flight request must return - not deadlock waiting on a closed socket |
| 290 | + PipesResult result = future.get(15, TimeUnit.SECONDS); |
| 291 | + assertNotNull(result, "In-flight parse should return a result, not hang " + repro); |
| 292 | + // Result may be SUCCESS (parse finished before close) or a crash/init-failure |
| 293 | + // status (close tore down the socket mid-flight) - both are acceptable. |
| 294 | + } finally { |
| 295 | + executor.shutdownNow(); |
| 296 | + } |
| 297 | + } |
| 298 | + |
219 | 299 | @Test |
220 | 300 | public void testPerClientModeStillWorks(@TempDir Path tmp) throws Exception { |
221 | 301 | // Verify default per-client mode still works |
|
0 commit comments