-
Notifications
You must be signed in to change notification settings - Fork 0
RAN-33: Hygiene cluster from RAN-6 review (5 LOW items batched) #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package io.github.randomcodespace.iq.analyzer; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.time.Duration; | ||
| import java.time.Instant; | ||
| import java.util.concurrent.CountDownLatch; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Executors; | ||
| import java.util.concurrent.Future; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| /** | ||
| * Regression coverage for {@link Analyzer.BoundedExecutor}: the default | ||
| * {@code ExecutorService.close()} can block up to 24 hours waiting for stuck | ||
| * ANTLR parser threads. The wrapper enforces a graceful 10s shutdown followed | ||
| * by a 5s {@code shutdownNow} window. | ||
| */ | ||
| class AnalyzerBoundedExecutorTest { | ||
|
|
||
| /** Hard upper bound on close() = graceful 10s + force 5s + scheduling slack. */ | ||
| private static final long MAX_CLOSE_SECONDS = 18; | ||
|
|
||
| @Test | ||
| void close_completes_within_bounded_window_for_long_running_task() throws Exception { | ||
| ExecutorService delegate = Executors.newSingleThreadExecutor(); | ||
| Analyzer.BoundedExecutor executor = new Analyzer.BoundedExecutor(delegate); | ||
|
|
||
| AtomicBoolean wasInterrupted = new AtomicBoolean(); | ||
| CountDownLatch started = new CountDownLatch(1); | ||
| Future<Void> task = executor.submit(() -> { | ||
| started.countDown(); | ||
| try { | ||
| // Far longer than the bounded close() window. | ||
| Thread.sleep(TimeUnit.MINUTES.toMillis(5)); | ||
|
Check warning on line 40 in src/test/java/io/github/randomcodespace/iq/analyzer/AnalyzerBoundedExecutorTest.java
|
||
| } catch (InterruptedException e) { | ||
|
Check warning on line 41 in src/test/java/io/github/randomcodespace/iq/analyzer/AnalyzerBoundedExecutorTest.java
|
||
| wasInterrupted.set(true); | ||
| Thread.currentThread().interrupt(); | ||
| } | ||
| return null; | ||
| }); | ||
| assertNotNull(task); | ||
| assertTrue(started.await(5, TimeUnit.SECONDS), "submitted task should start"); | ||
|
|
||
| Instant t0 = Instant.now(); | ||
| executor.close(); | ||
| Duration elapsed = Duration.between(t0, Instant.now()); | ||
|
|
||
| assertTrue(delegate.isShutdown(), | ||
| "delegate must be shutdown after close()"); | ||
| assertTrue(elapsed.toSeconds() < MAX_CLOSE_SECONDS, | ||
| "close() must respect bounded shutdown window (max " | ||
| + MAX_CLOSE_SECONDS + "s), got " + elapsed); | ||
| // shutdownNow should have interrupted the sleeping task. | ||
| assertTrue(wasInterrupted.get(), | ||
| "blocked task must be interrupted by shutdownNow"); | ||
| } | ||
|
|
||
| @Test | ||
| void close_is_immediate_when_executor_is_idle() { | ||
| ExecutorService delegate = Executors.newSingleThreadExecutor(); | ||
| Analyzer.BoundedExecutor executor = new Analyzer.BoundedExecutor(delegate); | ||
|
|
||
| Instant t0 = Instant.now(); | ||
| executor.close(); | ||
| Duration elapsed = Duration.between(t0, Instant.now()); | ||
|
|
||
| assertTrue(delegate.isShutdown()); | ||
| assertTrue(delegate.isTerminated()); | ||
| // Idle close should return well under the graceful window. | ||
| assertTrue(elapsed.toMillis() < 2_000, | ||
| "idle close() should return promptly, got " + elapsed); | ||
| } | ||
|
|
||
| @Test | ||
| void close_propagates_interrupt_to_caller_thread() throws Exception { | ||
| ExecutorService delegate = Executors.newSingleThreadExecutor(); | ||
| Analyzer.BoundedExecutor executor = new Analyzer.BoundedExecutor(delegate); | ||
|
|
||
| CountDownLatch started = new CountDownLatch(1); | ||
| executor.submit(() -> { | ||
| started.countDown(); | ||
| try { | ||
| Thread.sleep(TimeUnit.MINUTES.toMillis(5)); | ||
|
Check warning on line 89 in src/test/java/io/github/randomcodespace/iq/analyzer/AnalyzerBoundedExecutorTest.java
|
||
| } catch (InterruptedException ignored) { | ||
|
Check warning on line 90 in src/test/java/io/github/randomcodespace/iq/analyzer/AnalyzerBoundedExecutorTest.java
|
||
| // swallow — we're testing the wrapper, not the task | ||
| } | ||
| return null; | ||
| }); | ||
| assertTrue(started.await(5, TimeUnit.SECONDS)); | ||
|
|
||
| AtomicBoolean closerInterrupted = new AtomicBoolean(); | ||
| Thread closer = new Thread(() -> { | ||
| executor.close(); | ||
| closerInterrupted.set(Thread.currentThread().isInterrupted()); | ||
| }); | ||
| closer.start(); | ||
| // Let the closer enter awaitTermination, then interrupt it. | ||
| Thread.sleep(200); | ||
|
Check warning on line 104 in src/test/java/io/github/randomcodespace/iq/analyzer/AnalyzerBoundedExecutorTest.java
|
||
| closer.interrupt(); | ||
| closer.join(TimeUnit.SECONDS.toMillis(MAX_CLOSE_SECONDS)); | ||
|
|
||
| assertFalse(closer.isAlive(), "closer thread should finish after interrupt"); | ||
| assertTrue(delegate.isShutdown(), "interrupt path must still shutdown the delegate"); | ||
| assertTrue(closerInterrupted.get(), | ||
| "wrapper must restore the caller's interrupt flag"); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getEvidencenow canonicalizescodeiq.rootPathwithPath#toRealPath, which throws when the configured source directory is missing (for example, serving a graph artifact on a host without the original checkout). In that case any request withfile=...returns HTTP 500 before reachingassembler.assemble, even though the implementation explicitly supports logical-only graph references (it already ignoresNoSuchFileExceptionfor the candidate path). This is a behavior regression from the previous lexical guard and breaks valid file-based evidence queries in source-detached deployments.Useful? React with 👍 / 👎.