fix(core): track in-flight execute() playback so executeAsync cancels it - #95
Conversation
|
Warning Review limit reached
Next review available in: 36 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughHapticManager's execute() is reworked to route through executeAsync's handle-based path with a completion-polling loop and non-cancellable cleanup that only clears currentHandle if it still matches. FakeHapticExecutor/FakeHapticHandle gain time-based activity tracking, and tests are updated for coroutine time control and cancellation regression coverage. ChangesHandle-based execute/executeAsync cancellation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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.
🧹 Nitpick comments (1)
jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/HapticManager.kt (1)
214-220: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAvoid
runBlockinginside a coroutine — makeclearHandleIfCurrentsuspend.
clearHandleIfCurrentis only called fromexecute()'sfinallyblock insidewithContext(NonCancellable), which is a suspend context. UsingwithStateLockBlocking(runBlocking) here blocks the thread and risks deadlock in single-threaded dispatchers orrunTestifstateMutexis momentarily held by another coroutine on the same dispatcher. The Kotlin docs explicitly staterunBlockingshould not be used from a coroutine.Since the call site is already suspend, making this a
suspend funwithwithStateLockis a drop-in fix.♻️ Proposed refactor
- private fun clearHandleIfCurrent(handle: HapticHandle) { - withStateLockBlocking { + private suspend fun clearHandleIfCurrent(handle: HapticHandle) { + withStateLock { if (currentHandle === handle) currentHandle = null } }🤖 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 `@jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/HapticManager.kt` around lines 214 - 220, `clearHandleIfCurrent` currently uses the blocking `withStateLockBlocking`/`runBlocking` path inside a coroutine, which can deadlock on single-threaded dispatchers or `runTest`. Change `clearHandleIfCurrent` in `HapticManager` to a suspend function and switch it to `withStateLock` so the existing `execute()` finally block can call it without blocking. Keep the current handle identity check (`currentHandle === handle`) and preserve the cleanup behavior so newer `execute`/`executeAsync` handles are not cleared.
🤖 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.
Nitpick comments:
In
`@jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/HapticManager.kt`:
- Around line 214-220: `clearHandleIfCurrent` currently uses the blocking
`withStateLockBlocking`/`runBlocking` path inside a coroutine, which can
deadlock on single-threaded dispatchers or `runTest`. Change
`clearHandleIfCurrent` in `HapticManager` to a suspend function and switch it to
`withStateLock` so the existing `execute()` finally block can call it without
blocking. Keep the current handle identity check (`currentHandle === handle`)
and preserve the cleanup behavior so newer `execute`/`executeAsync` handles are
not cleared.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e5adf603-62a3-401a-8d13-d558cff12c8f
📒 Files selected for processing (3)
jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/HapticManager.ktjindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/HapticManagerTest.ktjindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/fake/FakeHapticExecutor.kt
Jindong Core Test Coverage Report
|
Closes #93.
executeclearedcurrentHandlebefore running playback, so during playback no handle pointed at the in-flight vibration.executeAsync, which only takesstateMutex, then saw a null handle, cancelled nothing, and fired a second vibration over the first.executenow drives its playback throughexecuteAsyncto get a real motor-stopping handle, publishes it intocurrentHandleunderstateMutex, then awaits completion; afinallyunderNonCancellablecancels the handle and clears the slot with an identity guard so it never wipes a handle a newer caller installed.This keeps the invariants:
executeAsyncnever touchesexecutionMutexand returns its handle synchronously, so the non-blocking contract holds.executetakesexecutionMutexthenstateMutex;executeAsynctakes onlystateMutex), so no new deadlock edge.NonCancellable.A handle is required rather than a coroutine
Jobbecauseexecutor.executedoes not callVibrator.cancel()on cancellation — onlyHapticHandle.cancel()stops the motor.Test
executeAsync should cancel an execute() playback that is still in flight: launchesexecutein the background, advances virtual time so it is provably playing, then callsexecuteAsyncand asserts the execute-issued handle is cancelled while the async handle survives. Reverting to the pre-fix behavior turns it red. Public API unchanged (internal concurrency only).Base:
main.Summary by CodeRabbit
Bug Fixes
Tests