Skip to content

Fix RejectedExecutionException crash on ViewModel teardown#432

Open
patrickrb wants to merge 1 commit into
devfrom
fix/qth-executor-shutdown
Open

Fix RejectedExecutionException crash on ViewModel teardown#432
patrickrb wants to merge 1 commit into
devfrom
fix/qth-executor-shutdown

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Problem

Pulled from a crashing device (debug.log + logcat crash buffer, 07-05). One of two distinct app crashes found:

FATAL EXCEPTION: Thread-139
java.util.concurrent.RejectedExecutionException: Task
  com.k1af.ft8af.MainViewModel$GetQTHRunnable rejected from
  ThreadPoolExecutor[Terminated, pool size = 0, ...]
    at com.k1af.ft8af.MainViewModel$4.afterDecode(MainViewModel.java)
    at com.k1af.ft8af.ft8listener.FT8SignalListener$4.run(FT8SignalListener.java:251)

A late deep-decode pass delivers afterDecode() on a background decode thread after MainViewModel.onCleared() has shut getQTHThreadPool / sendWaveDataThreadPool down (ViewModel destroyed on activity teardown/recreate). The raw execute() on the terminated pool throws RejectedExecutionException (default AbortPolicy), which is uncaught on the decode thread and crashes the process.

Fix

New SafeExecutor.tryExecute(pool, task) helper: fast-paths on isShutdown(), and wraps execute() in a try/catch (RejectedExecutionException) to cover the check→execute race with onCleared(). Drops the task and returns false instead of throwing.

Wired into all three submit sites in MainViewModel (1× getQTHThreadPool, 2× sendWaveDataThreadPool).

Tests

SafeExecutorTest (pure JUnit + Truth, no Robolectric needed): live-pool execution, shutdown pool, shutdownNow(), null pool, null task. :app:testDebugUnitTest green.

🤖 Generated with Claude Code

A late deep-decode pass can deliver afterDecode() on a background decode
thread after MainViewModel.onCleared() has already shut getQTHThreadPool /
sendWaveDataThreadPool down. The raw execute() on a terminated pool throws
RejectedExecutionException (default AbortPolicy), which is uncaught on the
decode thread and takes the whole process down.

Observed crash (07-05):
  RejectedExecutionException: Task MainViewModel$GetQTHRunnable rejected from
  ThreadPoolExecutor[Terminated] at MainViewModel.afterDecode

Guard all three pool submissions behind SafeExecutor.tryExecute(), a small
top-level helper that fast-paths on isShutdown() and swallows the
RejectedExecutionException race, dropping the task instead of crashing.
Extracted so it is unit-testable without Android (pure JUnit + Truth).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 21.57%. Comparing base (f23faaa) to head (eeaabbe).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##                dev     #432      +/-   ##
============================================
+ Coverage     21.47%   21.57%   +0.09%     
- Complexity      133      140       +7     
============================================
  Files           150      154       +4     
  Lines         19755    19945     +190     
  Branches       2909     2947      +38     
============================================
+ Hits           4243     4303      +60     
- Misses        15344    15474     +130     
  Partials        168      168              
Flag Coverage Δ
android 12.42% <ø> (+0.26%) ⬆️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

1 participant