Skip to content

fix(kotlin): stop public extensions from swallowing coroutine cancellation#572

Open
ayaangazali wants to merge 1 commit into
RunanywhereAI:mainfrom
ayaangazali:fix/kotlin-rethrow-cancellation
Open

fix(kotlin): stop public extensions from swallowing coroutine cancellation#572
ayaangazali wants to merge 1 commit into
RunanywhereAI:mainfrom
ayaangazali:fix/kotlin-rethrow-cancellation

Conversation

@ayaangazali

@ayaangazali ayaangazali commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to the pattern from #563: four public extension sites catch Throwable, which also traps kotlinx.coroutines.CancellationException (and JVM Errors). This breaks Kotlin's cooperative cancellation contract, a cancelled coroutine should stop, not keep going.

Sites

  1. loadModel (RunAnywhereModelLifecycle.kt:51): the ensureServicesReady() guard ate the caller's cancellation, so task.cancel() during init still ran the heavy native load afterwards. Now rethrows cancellation and swallows only Exception, same as the reviewed #563 registry fix.
  2. transcribeStream (RunAnywhereSTT.kt:112): collector cancellation got converted into a synthetic "STT stream failed" final partial. Now cancellation propagates; real errors still produce the failure partial and close the flow.
  3. synthesizeStream (RunAnywhereTTS.kt:131): normal cancellation logged a spurious "synthesizeStream errored" warning. Now rethrows; teardown is unchanged because close() lives in the finally.
  4. downloadModel cleanup (RunAnywhereDownload.kt:149): being honest, this one is NOT a cancellation bug. The block runs under NonCancellable and the original CancellationException keeps propagating after the finally either way. I only narrowed Throwable to Exception so the best-effort native cancel doesn't log-and-ignore JVM Errors. Happy to drop this hunk if you'd rather keep the change minimal.

Why the two-clause pattern

On the JVM CancellationException extends Exception (via java.util.concurrent.CancellationException), so just narrowing Throwable to Exception would still swallow it. The explicit catch (e: CancellationException) { throw e } clause first is required. Import used is kotlinx.coroutines.CancellationException, matching the existing usage in NativeUnaryRequest.kt:8 and ToolCallingOrchestrator.kt:31.

Testing

From a clean worktree of main: :compileDebugKotlin (no local natives), :ktlintCheck, :testDebugUnitTest, :detekt all pass.


Small note for transparency: I'm a freshman learning by contributing here. I found these by systematically comparing the Kotlin extensions against the Swift ones after you merged my earlier registry fix, and I bounced my reasoning off Claude Code before writing anything. If any of these catches were intentionally broad, tell me and I'll adjust or revert those hunks. Thanks for the quick reviews so far, it genuinely made my week :)

Summary by CodeRabbit

  • Bug Fixes
    • Improved cancellation handling across model loading, speech-to-text streaming, speech synthesis, and model downloads.
    • Canceled operations now stop cleanly instead of being reported as generic failures.
    • Preserved existing error reporting for non-cancellation failures.

…ation

Four public extension sites caught Throwable around suspending or
JNI-driving work, which also traps kotlinx CancellationException and
JVM Errors:

- loadModel (RunAnywhereModelLifecycle.kt): the ensureServicesReady()
  guard ate the caller's cancellation, so a cancelled coroutine went on
  to run the heavy native load anyway.
- transcribeStream (RunAnywhereSTT.kt): collector cancellation was
  converted into a synthetic "STT stream failed" final partial instead
  of propagating as a cancel.
- synthesizeStream (RunAnywhereTTS.kt): normal cancellation was logged
  as a spurious "synthesizeStream errored" warning; flow teardown is
  unchanged since close() stays in the finally block.
- downloadModel cleanup (RunAnywhereDownload.kt): no cancellation bug
  here (the block runs under NonCancellable and the original exception
  keeps propagating after the finally), but the best-effort native
  cancel now narrows to Exception so JVM Errors are not logged-and-
  ignored.

Pattern matches the reviewed fix in RunanywhereAI#563: rethrow CancellationException
first, then handle Exception. On the JVM CancellationException extends
Exception, so the explicit rethrow clause is required, not just a
Throwable-to-Exception narrowing.

Gates: :compileDebugKotlin (no local natives), :ktlintCheck,
:testDebugUnitTest, :detekt all green.

Signed-off-by: ayaangazali <ayaangazali.work@gmail.com>
Copilot AI review requested due to automatic review settings July 19, 2026 17:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Kotlin SDK exception handling now preserves coroutine cancellation in model loading, STT streaming, and TTS streaming, while native download cancellation narrows its catch block from Throwable to Exception.

Changes

Coroutine cancellation handling

Layer / File(s) Summary
Propagate cancellation across SDK operations
sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/{Models,STT,TTS,Storage}/*
Model loading, STT streaming, and TTS streaming rethrow CancellationException; native download cancellation catches Exception instead of Throwable.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: copilot, sanchitmonga22

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change, but it does not follow the repository template sections or required checklists. Add the required template sections: Description, Type of Change, Testing, platform-specific testing, Labels, Checklist, and Screenshots.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: preserving coroutine cancellation in public Kotlin extensions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/TTS/RunAnywhereTTS.kt (1)

151-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow exception handling to avoid swallowing JVM errors.

Consider replacing catch (t: Throwable) with catch (t: Exception) for the native stop() call, aligning with the native download cancellation changes in this PR. Based on learnings, you should avoid catching Throwable/Error to ensure that critical JVM Error subclasses (like OutOfMemoryError) are allowed to propagate.

♻️ Proposed refactor
-                    } catch (t: Throwable) {
+                    } catch (t: Exception) {
🤖 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
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/TTS/RunAnywhereTTS.kt`
at line 151, Update the exception handler around the native TTS stop call in
RunAnywhereTTS to catch Exception instead of Throwable, allowing critical JVM
Error subclasses to propagate while preserving handling of ordinary operation
failures.

Source: Learnings

🤖 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
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/TTS/RunAnywhereTTS.kt`:
- Line 151: Update the exception handler around the native TTS stop call in
RunAnywhereTTS to catch Exception instead of Throwable, allowing critical JVM
Error subclasses to propagate while preserving handling of ordinary operation
failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6fdd15c9-0e93-44a1-a24a-d9488b8b0524

📥 Commits

Reviewing files that changed from the base of the PR and between 541842a and 129461a.

📒 Files selected for processing (4)
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Models/RunAnywhereModelLifecycle.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/STT/RunAnywhereSTT.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/RunAnywhereDownload.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/TTS/RunAnywhereTTS.kt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants