Skip to content

cleanup(android): rethrow CancellationException in UriContentReader - #1329

Merged
9thLevelSoftware merged 2 commits into
mainfrom
cursor/android-uri-reader-cancellation-1021
Oct 23, 2026
Merged

9thLevelSoftware merged 2 commits into
mainfrom
cursor/android-uri-reader-cancellation-1021

Conversation

@9thLevelSoftware

@9thLevelSoftware 9thLevelSoftware commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

readUriContent and readUriContentUpTo run a blocking provider read inside withContext(Dispatchers.IO) and catch Exception. Cancelling the caller never threw inside that try. withContext raised the cancellation only after the read had run to completion. A rethrow alone would therefore never fire.

Both reads now run inside runInterruptible, as AndroidCsvImportIntake.readBounded already does for the same readUpTo helper. Cancelling the caller interrupts the reading thread. A stream that honours interrupts stops early, and the cancellation surfaces inside the try, where kotlinx.coroutines.CancellationException is rethrown before the Exception handler. Other failures still return null / Unreadable. A provider stream that ignores interrupts (for example one backed by a FileInputStream) still finishes its read before the cancellation propagates, as before. The iOS reader and CsvImporter are unchanged.

UriContentReaderCancellationTest covers:

  • an in-flight read that is interrupted when the caller is cancelled, for both readers;
  • a provider-thrown CancellationException;
  • an ordinary IO failure;
  • a successful read.

A cancelled picker read was caught as Exception and returned as null or
Unreadable. Rethrow CancellationException so cancellation propagates.

Co-authored-by: Devil <9thLevelSoftware@users.noreply.github.com>
@9thLevelSoftware
9thLevelSoftware marked this pull request as ready for review October 23, 2026 14:31
Copilot AI balanced review requested due to automatic review settings October 23, 2026 14:31

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.

@kilo-code-bot

kilo-code-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 2 Issues Found | Recommendation: Merge (optional cleanup only)

Verdict: Approve - both previous WARNING findings are fixed at 47c54dcb (both reads now run inside runInterruptible, and the new tests cancel the caller mid-read and assert the interrupt); the two remaining findings are optional test cleanup.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
shared/src/androidHostTest/kotlin/com/devil/phoenixproject/util/UriContentReaderCancellationTest.kt 88 The new interruption test's last assertion (assertFailsWith<CancellationException> { read.await() }) cannot fail for any implementation of the reader: read.cancel() cancels the deferred itself, so await() throws whether the reader rethrows the cancellation or swallows it and maps the read to null. The interrupted latch proves the interrupt; nothing in the test proves the PR's headline claim that the cancellation is rethrown rather than mapped to null. Fix: drop these two assertions (L88, L100) or capture the reader's own outcome inside its own scope and assert on that; the provider-thrown-cancellation tests already cover the rethrow.
shared/src/androidHostTest/kotlin/com/devil/phoenixproject/util/UriContentReaderCancellationTest.kt 143 BlockingStream.read() duplicates InputStream.read(), which already delegates to the overridden read(byte[], int, int), and no production read path calls the no-arg overload. Fix: delete the no-arg override.
Files Reviewed (2 files)
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/util/UriContentReader.android.kt - 0 issues
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/util/UriContentReaderCancellationTest.kt - 2 issues

Previous Findings (re-verified at 47c54dcb)

  • UriContentReader.android.kt L25/L37: both blocking reads are now wrapped in runInterruptible, so the catch (e: CancellationException) { throw e } rethrow is reachable - resolved.
  • UriContentReaderCancellationTest.kt L79/L92: new tests cancel the reading coroutine while the provider read is in flight and assert the read was interrupted - resolved.
  • UriContentReader.android.kt import: now kotlinx.coroutines.CancellationException, matching FilePicker.android.kt and AndroidCsvImportIntake.kt - resolved.

Ponytail Review

  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/util/UriContentReaderCancellationTest.kt:L88: test-shrink: assertFailsWith<CancellationException> { read.await() } asserts Deferred behaviour, not reader behaviour. Delete it or replace it with an assertion on the reader's own outcome.
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/util/UriContentReaderCancellationTest.kt:L143: delete: the no-arg read() override duplicates InputStream.read(), which already delegates to the overridden array overload. Remove it.

Ponytail net: -1 lines.

Suggested Minimal Patch

One file, no new dependencies:

  • UriContentReaderCancellationTest.kt: delete the no-arg read() override (L143) and the two assertFailsWith<CancellationException> { read.await() } lines (L88, L100), or replace the latter with an assertion on the reader's own captured outcome.

Final Merge Guidance

Can merge as-is; both findings are optional test cleanup.

Fix these issues in Kilo Cloud

Previous Review Summary (commit 7971232)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 7971232)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
shared/src/androidMain/kotlin/com/devil/phoenixproject/util/UriContentReader.android.kt 24 The new CancellationException rethrow cannot fire for the cancellation this PR targets: the provider read is a blocking call, so a cancelled coroutine never throws inside the try and withContext raises the cancellation itself, outside this catch. The old code already propagated coroutine cancellation. Fix: wrap the reads in runInterruptible (precedent: shared/src/androidMain/kotlin/com/devil/phoenixproject/data/csv/AndroidCsvImportIntake.kt:85) and keep the rethrow, which then becomes meaningful.
shared/src/androidHostTest/kotlin/com/devil/phoenixproject/util/UriContentReaderCancellationTest.kt 48 The test only exercises a stream that throws CancellationException itself, not the PR's stated scenario (a cancelled picker read). No test cancels the reading coroutine, so the suite passes while a cancelled read still runs to completion. Fix: add a test that cancels the reading coroutine mid-read and asserts the blocking read is interrupted.

SUGGESTION

File Line Issue
shared/src/androidMain/kotlin/com/devil/phoenixproject/util/UriContentReader.android.kt 6 kotlin.coroutines.cancellation.CancellationException duplicates the import this package already uses (kotlinx.coroutines.CancellationException in util/FilePicker.android.kt:13 and data/csv/AndroidCsvImportIntake.kt:15). Same type on the JVM; one import keeps cancellation rethrows greppable.
Files Reviewed (2 files)
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/util/UriContentReader.android.kt - 2 issues
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/util/UriContentReaderCancellationTest.kt - 1 issue

Ponytail Review

Ponytail: Lean already. Ship.

Ponytail net: 0 lines.

Suggested Minimal Patch

One file of production change plus one new test, no new dependencies:

  • UriContentReader.android.kt: wrap both blocking reads in runInterruptible { ... } (L21, L34), keep the two catch (e: CancellationException) { throw e } blocks.
  • UriContentReaderCancellationTest.kt: add one test that cancels the reading coroutine while the provider read is in flight and asserts the read is interrupted and the coroutine ends cancelled.
  • Optional: swap the cancellation import to kotlinx.coroutines.CancellationException.

Final Merge Guidance

Can merge after the two WARNING findings are addressed; the SUGGESTION is optional cleanup.

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 90.9K · Output: 44.6K · Cached: 1M

Review guidance: REVIEW.md from base branch main

The provider read inside withContext(IO) is blocking, so cancelling the
caller never threw inside the try: withContext raised the cancellation
after the read finished, and the new rethrow could not fire. Wrap both
reads in runInterruptible, as AndroidCsvImportIntake.readBounded already
does, so cancellation interrupts the reading thread and surfaces inside the
try where it is rethrown.

Add tests that cancel the caller while the provider read is in flight and
assert the read is interrupted and the caller ends cancelled. Import
kotlinx.coroutines.CancellationException like the other Android readers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tsDJ6LzV6FwGD3VQwfNEn
@9thLevelSoftware
9thLevelSoftware merged commit 07a3072 into main Oct 23, 2026
11 checks passed
@9thLevelSoftware
9thLevelSoftware deleted the cursor/android-uri-reader-cancellation-1021 branch October 23, 2026 17:40
read.cancel()

assertTrue(stream.interrupted.await(LATCH_SECONDS, TimeUnit.SECONDS), "blocking read was not interrupted")
assertFailsWith<CancellationException> { read.await() }

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.

SUGGESTION: This assertion cannot fail for any implementation of the reader

read.cancel() cancels the deferred itself, so read.await() throws a CancellationException whether the reader rethrows the cancellation or swallows it and maps the read to null. The assertion therefore checks Deferred behaviour, not this PR's change; the interrupted latch on the line above is the assertion that actually proves the blocking read was interrupted. The same applies to the identical assertion at L100.

Fix: drop these two assertions, or make them prove the reader's own outcome - capture the value/exception inside the reader's own scope (e.g. complete a CompletableDeferred with it) and assert on that instead of on the cancelled deferred. The provider-thrown-cancellation tests already cover the rethrow.

val started = CountDownLatch(1)
val interrupted = CountDownLatch(1)

override fun read(): Int = block()

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.

SUGGESTION: The no-arg read() override duplicates InputStream.read()

InputStream.read() already delegates to read(byte[], int, int), which this class overrides, and neither production read path calls the no-arg overload. Delete this override and keep the array one. (optional cleanup)

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.

4 participants