Skip to content

ADFA-2539 Sentry APPDEVFORALL-27 safer exception handling#1222

Merged
hal-eisen-adfa merged 4 commits into
stagefrom
ADFA-2539-UncheckedIOException-ZipFile-CleanableResource
Apr 21, 2026
Merged

ADFA-2539 Sentry APPDEVFORALL-27 safer exception handling#1222
hal-eisen-adfa merged 4 commits into
stagefrom
ADFA-2539-UncheckedIOException-ZipFile-CleanableResource

Conversation

@hal-eisen-adfa
Copy link
Copy Markdown
Collaborator

ZipFileSystem.java:

  • finalize(): catch (IOException) → catch (Throwable) which prevents UncheckedIOException from killing the FinalizerDaemon thread
  • close(): wrapped ch.close() to catch IOException | UncheckedIOException to prevent EIO on channel close from propagating

CachedJarFileSystem.kt:

  • doClose(): catch (e: IOException) → catch (e: Exception) now catches UncheckedIOException and other RuntimeExceptions during explicit cleanup

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Safer Exception Handling for Zip File Operations

Key Changes

  • Introduced closeChannelQuietly() helper method in ZipFileSystem to safely close SeekableByteChannel instances without propagating IOException or UncheckedIOException, preventing cleanup-time failures from crashing the FinalizerDaemon thread on devices with flaky storage.

  • Enhanced finalize() exception handling in ZipFileSystem to catch Throwable (instead of just IOException) to ensure all exceptions are swallowed during finalization and prevent terminating the FinalizerDaemon thread.

  • Updated exception handling in CachedJarFileSystem.doClose() to catch both IOException and UncheckedIOException separately, with warning-level logging for visibility into failures during explicit cleanup.

  • Centralized quiet-close pattern applied in three locations: ZipFileSystem.close(), ZipFileSystem.sync(), and ExistingChannelCloser.closeAndDeleteIfDone() for consistency and resilience.

Risks and Best Practice Concerns

⚠️ Silent exception swallowing in finalize() - While necessary to prevent thread termination, catching all Throwable in finalize() is an aggressive pattern that may hide unexpected errors. Consider monitoring for unusual finalization behavior.

⚠️ Swallowed errors in closeChannelQuietly() - Silently discarding IOException and UncheckedIOException during channel closure can mask underlying storage issues. Consider adding debug-level logging or metrics collection to track these failures.

⚠️ Underlying storage reliability issue - The PR description indicates this is a workaround for flaky storage devices that wrap EIO in UncheckedIOException. Consider investigating root causes and device compatibility rather than relying solely on exception suppression.

⚠️ Inconsistent exception handling patterns - Different modules use slightly different approaches (ZipFileSystem uses single catch block; CachedJarFileSystem uses separate catch blocks with explicit logging), which may make future maintenance more difficult.

Walkthrough

Updated ZIP/JAR filesystem cleanup to use a quiet close helper and broadened exception handling: close operations now swallow I/O errors (including UncheckedIOException) and the ZipFileSystem finalizer catches all Throwables to avoid finalizer-thread termination.

Changes

Cohort / File(s) Summary
Zip filesystem cleanup
composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java
Added closeChannelQuietly(SeekableByteChannel) and replaced direct ch.close() usages (in close(), sync(), and deletion/closer paths) to swallow I/O errors; finalize() now catches Throwable instead of IOException.
Cached JAR FS close handling
subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt
Expanded doClose() exception handling to also catch java.io.UncheckedIOException (logs a warning and preserves existing control flow).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Suggested reviewers

  • itsaky-adfa
  • Daniel-ADFA

Poem

🐰 I nibble at bytes, hush the noisy close,
I tuck stray IOs where nobody knows,
Finalizers sleep while threads softly hum,
Zip doors shut quiet — hop, code is done.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title references ticket numbers (ADFA-2539, APPDEVFORALL-27) and describes the main theme of safer exception handling, which aligns with the changeset focused on preventing exceptions during resource cleanup.
Description check ✅ Passed The description is directly related to the changeset, detailing specific exception handling improvements in ZipFileSystem.java and CachedJarFileSystem.kt that match the actual changes made.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-2539-UncheckedIOException-ZipFile-CleanableResource

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java (1)

292-301: ⚠️ Potential issue | 🟠 Major

Cover the ch.close() call inside sync() as well.

This wrapper only protects the post-sync() close. When hasUpdate is true, sync() can close ch internally before returning, so the same UncheckedIOException can still escape and skip the remaining cleanup in close().

Suggested helper-based fix
+    private void closeChannelIgnoringNonFatal(SeekableByteChannel channel) {
+        try {
+            channel.close();
+        } catch (IOException | UncheckedIOException ignored) {
+            // EIO on close is non-fatal — the channel is being released regardless
+        }
+    }
+
@@
-            try {
-                ch.close();              // close the ch just in case no update
-                // and sync didn't close the ch
-            } catch (IOException | UncheckedIOException ignored) {
-                // EIO on close is non-fatal — the channel is being released regardless
-            }
+            closeChannelIgnoringNonFatal(ch); // close just in case sync didn't

Then use the same helper for the internal close path in sync():

-            ch.close();
+            closeChannelIgnoringNonFatal(ch);
             Files.delete(zfpath);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java`
around lines 292 - 301, The ch.close() invocation after
AccessController.doPrivileged should be moved into (or replaced by) a small
privileged-close helper so both the external post-sync close and any internal
close() calls inside sync() use the same AccessController.doPrivileged wrapper
and consistent exception handling; add a private helper like private Void
doPrivilegedClose(SeekableByteChannel ch) (or similar) that calls ch.close()
inside AccessController.doPrivileged and catches/unwraps IOException into
UncheckedIOException as appropriate, then call that helper from the outer close
block (replacing the direct ch.close()) and from the internal close path inside
sync() so UncheckedIOException cannot escape and cleanup continues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt`:
- Around line 54-55: The catch in CachedJarFileSystem's doClose()/close path is
too broad; replace the generic catch (e: Exception) with explicit catches for
java.io.IOException and java.io.UncheckedIOException and log those (using the
existing log.warn call), and allow any other unexpected exceptions to propagate
(rethrow) so they fail fast; locate the catch in CachedJarFileSystem (the
doClose/close cleanup block) and change the error handling accordingly.

---

Outside diff comments:
In
`@composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java`:
- Around line 292-301: The ch.close() invocation after
AccessController.doPrivileged should be moved into (or replaced by) a small
privileged-close helper so both the external post-sync close and any internal
close() calls inside sync() use the same AccessController.doPrivileged wrapper
and consistent exception handling; add a private helper like private Void
doPrivilegedClose(SeekableByteChannel ch) (or similar) that calls ch.close()
inside AccessController.doPrivileged and catches/unwraps IOException into
UncheckedIOException as appropriate, then call that helper from the outer close
block (replacing the direct ch.close()) and from the internal close path inside
sync() so UncheckedIOException cannot escape and cleanup continues.
🪄 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: 9564a7c0-747c-4a0d-8cce-49f821d242da

📥 Commits

Reviewing files that changed from the base of the PR and between 644e091 and bfb49c8.

📒 Files selected for processing (2)
  • composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java
  • subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt

@hal-eisen-adfa
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java (2)

1090-1099: Narrow catch (Throwable) to catch (Exception) in finalize().

The stated goal is to swallow UncheckedIOException (a RuntimeException) from close() so the FinalizerDaemon isn’t killed. Catching Throwable additionally swallows Error subclasses (e.g. OutOfMemoryError, StackOverflowError, LinkageError, ThreadDeath), which can hide serious JVM-level problems and is generally discouraged. Exception is sufficient to cover both IOException from the declared signature and any RuntimeException/UncheckedIOException thrown through close().

Based on learnings from DefaultActionsRegistry.kt (PR #1018): the team prefers narrow exception handling targeted at the specific types documented in the crash report, rather than blanket catches.

♻️ Proposed narrowing
     `@SuppressWarnings`("deprecation")
     protected void finalize() throws IOException {
         try {
             close();
-        } catch (Throwable ignored) {
+        } catch (Exception ignored) {
             // On devices with flaky storage, close() can throw UncheckedIOException
             // wrapping EIO. Swallow all errors during finalization to prevent
             // killing the FinalizerDaemon thread.
         }
     }

Optionally, log at warn level here as well so finalizer-time close failures remain diagnosable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java`
around lines 1090 - 1099, In ZipFileSystem's finalize() method, replace the
overly-broad catch(Throwable) with catch(Exception) so we swallow IOException
and runtime exceptions like UncheckedIOException from close() but do not
suppress JVM Errors; locate the protected void finalize() implementation and
change the catch clause to catch Exception (optionally adding a
processLogger.warn or similar inside the catch to record finalizer-time close
failures for diagnostics) while keeping the call to close() and the swallowing
behavior.

1078-1088: Consider logging swallowed close failures for observability.

closeChannelQuietly discards both IOException and UncheckedIOException with no trace, which makes flaky-storage issues invisible in logs/bug reports and diverges from the sibling pattern in CachedJarFileSystem.kt (see closeJarFs, which logs each exception type via log.warn). A low-volume logger.warn(...) here would preserve the “quiet for the caller, but visible to diagnostics” behavior without reopening Sentry tickets.

Based on learnings from IDEApplication.kt (PR #957): non-fatal GC cleanup I/O errors should be logged with logger.warn and not reported to Sentry. The current fully silent swallow drops even the warn-level signal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java`
around lines 1078 - 1088, The closeChannelQuietly method in ZipFileSystem
currently swallows IOException and UncheckedIOException silently; update it to
log a low-volume warning instead (e.g., via the class logger) so flaky-storage
close failures are visible for diagnostics but still not propagated or sent to
Sentry; specifically, in ZipFileSystem.closeChannelQuietly(SeekableByteChannel)
catch each exception and call logger.warn with a concise message and the caught
exception (do not rethrow or report to Sentry) to match the pattern used by
CachedJarFileSystem.closeJarFs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java`:
- Around line 1090-1099: In ZipFileSystem's finalize() method, replace the
overly-broad catch(Throwable) with catch(Exception) so we swallow IOException
and runtime exceptions like UncheckedIOException from close() but do not
suppress JVM Errors; locate the protected void finalize() implementation and
change the catch clause to catch Exception (optionally adding a
processLogger.warn or similar inside the catch to record finalizer-time close
failures for diagnostics) while keeping the call to close() and the swallowing
behavior.
- Around line 1078-1088: The closeChannelQuietly method in ZipFileSystem
currently swallows IOException and UncheckedIOException silently; update it to
log a low-volume warning instead (e.g., via the class logger) so flaky-storage
close failures are visible for diagnostics but still not propagated or sent to
Sentry; specifically, in ZipFileSystem.closeChannelQuietly(SeekableByteChannel)
catch each exception and call logger.warn with a concise message and the caught
exception (do not rethrow or report to Sentry) to match the pattern used by
CachedJarFileSystem.closeJarFs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 445fc72b-1cc0-479b-96b0-6881ff8ffc1d

📥 Commits

Reviewing files that changed from the base of the PR and between bfb49c8 and cb42d6b.

📒 Files selected for processing (2)
  • composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/zipfs2/ZipFileSystem.java
  • subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt

@hal-eisen-adfa hal-eisen-adfa merged commit 6572429 into stage Apr 21, 2026
2 checks passed
@hal-eisen-adfa hal-eisen-adfa deleted the ADFA-2539-UncheckedIOException-ZipFile-CleanableResource branch April 21, 2026 14:12
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.

3 participants