ADFA-3863 Fix LayoutInflaterTest hang that wedged Jacoco/SonarQube CI#1371
ADFA-3863 Fix LayoutInflaterTest hang that wedged Jacoco/SonarQube CI#1371hal-eisen-adfa wants to merge 1 commit into
Conversation
The analyze.yml workflow was timing out at the 180-minute job limit because :xml-inflater:testV8DebugUnitTest hung waiting on a Tooling API child JVM that never exited. Add defense in depth so a future hang fails in minutes, not hours, and fix the underlying race. Safety nets: - ToolingApiTestLauncher: bound cancelCurrentBuild()/shutdown() RPCs with 30s timeouts and fall back to proc.destroyForcibly() if the child JVM does not exit. The child can no longer leak regardless of RPC behavior. - LayoutInflaterTest: @rule Timeout caps each test at 2 minutes. - build.gradle.kts: project-wide Test task timeout of 10 minutes as a final backstop. - analyze.yml: per-step timeout of 60 minutes on "Build and analyze" so a wedge cannot burn the full 180-minute job budget. Root cause: - XmlInflaterTest.initIfNeeded used a non-atomic get/set pair on AtomicBoolean. Replace with compareAndSet, and reset the flag on failure so a failed init does not silently poison subsequent tests.
📝 WalkthroughRelease NotesCI Reliability Improvements
Bug Fix
Risk Notes
WalkthroughThe PR hardens the CI pipeline against indefinite test hangs by introducing timeout protection at four independent levels: workflow step timeout, Gradle task timeout, test launcher cleanup timeout, and individual test method timeout. Additionally, XmlInflaterTest initialization is refactored to use atomic compare-and-set semantics, allowing retries when setup fails instead of permanently blocking. ChangesLayered Test Timeout Protection
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.github/workflows/analyze.yml (1)
95-100: ⚡ Quick winApply the same backstop to the earlier Gradle step.
Build and analyzeis now bounded, butAssemble V8 Debugis still an unbounded./gradlewinvocation. A hang there would still sit until the 180-minute job timeout, so the workflow only has partial protection right now.Suggested change
- name: Assemble V8 Debug + timeout-minutes: 60 run: | echo "gradle_time_start=$(date +%s)" >> $GITHUB_ENV flox activate -d flox/base -- ./gradlew :app:assembleV8Debug --no-daemon echo "gradle_time_end=$(date +%s)" >> $GITHUB_ENV🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/analyze.yml around lines 95 - 100, The earlier unbounded Gradle step "Assemble V8 Debug" needs the same timeout and env backstop as the "Build and analyze" step; update the "Assemble V8 Debug" job/step to include timeout-minutes: 60 and the GRADLE_OPTS and SONAR_TOKEN env entries (or at least GRADLE_OPTS="-Xmx10g -XX:MaxMetaspaceSize=512m") and keep the existing run command that invokes ./gradlew (the Assemble V8 Debug step) so the Gradle invocation is bounded and uses the same memory/metaspace settings as the Build and analyze step.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt`:
- Around line 40-44: The current init.compareAndSet(false, true) lets
non-winning callers return immediately and use the inflater before setup
finishes; change the init logic so losers block until initialization completes
(success or failure) instead of returning. Replace the AtomicBoolean init with a
state machine or synchronization primitive (e.g., an enum/AtomicInteger states
like UNINITIALIZED/INITIALIZING/READY/FAILED plus wait/notify or a
CountDownLatch) and update the initialization block and all callers to: the
winning thread performs setup and sets the terminal state (READY or FAILED),
then notifies/writes the latch, while non-winning threads wait for that terminal
state and then proceed or throw based on success/failure; ensure you update the
same init usage shown in XmlInflaterTest.kt (the init variable and the init
compareAndSet block spanning the original lines and the related 46-55 section).
---
Nitpick comments:
In @.github/workflows/analyze.yml:
- Around line 95-100: The earlier unbounded Gradle step "Assemble V8 Debug"
needs the same timeout and env backstop as the "Build and analyze" step; update
the "Assemble V8 Debug" job/step to include timeout-minutes: 60 and the
GRADLE_OPTS and SONAR_TOKEN env entries (or at least GRADLE_OPTS="-Xmx10g
-XX:MaxMetaspaceSize=512m") and keep the existing run command that invokes
./gradlew (the Assemble V8 Debug step) so the Gradle invocation is bounded and
uses the same memory/metaspace settings as the Build and analyze step.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c366db36-381c-4eff-b6c9-e2a814a48bc9
📒 Files selected for processing (5)
.github/workflows/analyze.ymlbuild.gradle.ktstesting/tooling/src/main/java/com/itsaky/androidide/testing/tooling/ToolingApiTestLauncher.ktxml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.ktxml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt
| // Atomic claim of the init slot. If another caller already won the race, | ||
| // they are responsible for completing setup; we just return. | ||
| if (!init.compareAndSet(false, true)) { | ||
| return | ||
| } |
There was a problem hiding this comment.
Non-winning callers can continue before initialization finishes.
compareAndSet prevents duplicate init, but losers return immediately and may use the inflater before setup completes. This creates a race/flaky path when tests run in parallel. Use a wait/notify (or latch/state-machine) so non-winning callers block until init reaches success/failure.
Suggested fix (state machine + wait/notify)
object XmlInflaterTest {
- private val init = AtomicBoolean(false)
+ private enum class InitState { UNINITIALIZED, INITIALIZING, INITIALIZED }
+ private val initLock = Object()
+ `@Volatile` private var initState = InitState.UNINITIALIZED
internal val activity by lazy { Robolectric.buildActivity(AppCompatActivity::class.java).get() }
fun initIfNeeded() {
- // Atomic claim of the init slot. If another caller already won the race,
- // they are responsible for completing setup; we just return.
- if (!init.compareAndSet(false, true)) {
- return
- }
+ synchronized(initLock) {
+ while (initState == InitState.INITIALIZING) initLock.wait()
+ if (initState == InitState.INITIALIZED) return
+ initState = InitState.INITIALIZING
+ }
try {
ToolingApiTestLauncher.launchServer {
assertThat(result is InitializeResult.Success).isTrue()
runBlocking { IProjectManager.getInstance().setup(gradleBuild.get()) }
}
+ synchronized(initLock) {
+ initState = InitState.INITIALIZED
+ initLock.notifyAll()
+ }
} catch (error: Throwable) {
- // Roll back so subsequent tests can retry rather than silently skipping setup.
- init.set(false)
+ synchronized(initLock) {
+ initState = InitState.UNINITIALIZED
+ initLock.notifyAll()
+ }
throw error
}
}
}Also applies to: 46-55
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt`
around lines 40 - 44, The current init.compareAndSet(false, true) lets
non-winning callers return immediately and use the inflater before setup
finishes; change the init logic so losers block until initialization completes
(success or failure) instead of returning. Replace the AtomicBoolean init with a
state machine or synchronization primitive (e.g., an enum/AtomicInteger states
like UNINITIALIZED/INITIALIZING/READY/FAILED plus wait/notify or a
CountDownLatch) and update the initialization block and all callers to: the
winning thread performs setup and sets the terminal state (READY or FAILED),
then notifies/writes the latch, while non-winning threads wait for that terminal
state and then proceed or throw based on success/failure; ensure you update the
same init usage shown in XmlInflaterTest.kt (the init variable and the init
compareAndSet block spanning the original lines and the related 46-55 section).
The analyze.yml workflow was timing out at the 180-minute job limit because :xml-inflater:testV8DebugUnitTest hung waiting on a Tooling API child JVM that never exited. Add defense in depth so a future hang fails in minutes, not hours, and fix the underlying race.
Safety nets:
Root cause: