Repository navigation
cleanup(android): rethrow CancellationException in UriContentReader - #1329
Conversation
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>
Code Review SummaryStatus: 2 Issues Found | Recommendation: Merge (optional cleanup only) Verdict: Approve - both previous WARNING findings are fixed at Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (2 files)
Previous Findings (re-verified at
|
| 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 issuesshared/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 inrunInterruptible { ... }(L21, L34), keep the twocatch (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.
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
| read.cancel() | ||
|
|
||
| assertTrue(stream.interrupted.await(LATCH_SECONDS, TimeUnit.SECONDS), "blocking read was not interrupted") | ||
| assertFailsWith<CancellationException> { read.await() } |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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)
readUriContentandreadUriContentUpTorun a blocking provider read insidewithContext(Dispatchers.IO)and catchException. Cancelling the caller never threw inside thattry.withContextraised the cancellation only after the read had run to completion. A rethrow alone would therefore never fire.Both reads now run inside
runInterruptible, asAndroidCsvImportIntake.readBoundedalready does for the samereadUpTohelper. Cancelling the caller interrupts the reading thread. A stream that honours interrupts stops early, and the cancellation surfaces inside thetry, wherekotlinx.coroutines.CancellationExceptionis rethrown before theExceptionhandler. Other failures still return null /Unreadable. A provider stream that ignores interrupts (for example one backed by aFileInputStream) still finishes its read before the cancellation propagates, as before. The iOS reader andCsvImporterare unchanged.UriContentReaderCancellationTestcovers:CancellationException;