Skip to content

Fix crash: guard executor submits against teardown race#324

Closed
patrickrb wants to merge 1 commit into
devfrom
fix/qth-pool-rejected-execution
Closed

Fix crash: guard executor submits against teardown race#324
patrickrb wants to merge 1 commit into
devfrom
fix/qth-pool-rejected-execution

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Problem

The app crashes with RejectedExecutionException after a decode when the connection is reset/reconnected or the app is closing. Observed live on a FlexRadio network reconnect:

FATAL EXCEPTION: Thread-101
java.util.concurrent.RejectedExecutionException: Task ...GetQTHRunnable rejected from
   ThreadPoolExecutor[Terminated, pool size = 0 ...]
   at MainViewModel$4.afterDecode(MainViewModel.java:594)
   at FT8SignalListener$3.run(FT8SignalListener.java:189)

afterDecode() runs on the FT8 decode thread and submits a QTH/grid-lookup task to getQTHThreadPool after every pass. That pool — and sendWaveDataThreadPool — is shut down in MainViewModel.onCleared() when the ViewModel is destroyed (app close, connection reset/reconnect). A decode result that lands in the same instant the pool is shut down reaches ExecutorService.execute() on a terminated pool and throws on the decode thread, crashing the app.

Fix

Add ExecutorUtils.safeExecute(pool, task), which:

  • skips submission when pool.isShutdown(), and
  • catches the residual RejectedExecutionException for the narrow check-then-execute race,

dropping the follow-up task instead of crashing. The dropped work is non-essential (a grid lookup, or a TX-audio send during teardown).

Routed all three submit sites through it: the QTH lookup (line 594) and the two network/CAT TX-audio sends (lines 689, 717).

Test

ExecutorUtilsTest (pure JUnit4 + Truth, no runner needed): active pool runs the task and returns true; shut-down pool drops the task and returns false without throwing; null pool/task return false.

🤖 Generated with Claude Code

afterDecode() runs on the FT8 decode thread and submits a QTH-lookup
task to getQTHThreadPool after every decode pass. That pool (and
sendWaveDataThreadPool) is shut down in MainViewModel.onCleared() when
the ViewModel is destroyed on app close or connection reset/reconnect. A
decode result landing in the same instant the pool is shut down reaches
ExecutorService.execute() on a terminated pool and throws
RejectedExecutionException on the decode thread, crashing the app
(observed on a FlexRadio network reconnect).

Add ExecutorUtils.safeExecute(), which skips submission when the pool is
shut down and swallows the residual RejectedExecutionException race,
dropping the follow-up task instead of crashing. Route all three submit
sites (QTH lookup + two network/CAT TX-audio sends) through it.

Covered by ExecutorUtilsTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 11.51%. Comparing base (bc6c21e) to head (8dd0103).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##                dev     #324   +/-   ##
=========================================
  Coverage     11.51%   11.51%           
  Complexity      113      113           
=========================================
  Files            88       88           
  Lines         12466    12466           
  Branches       2230     2230           
=========================================
  Hits           1435     1435           
  Misses        10900    10900           
  Partials        131      131           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI 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.

Pull request overview

This PR prevents a teardown race from crashing the decode thread by guarding ExecutorService.execute() submissions against executors that are concurrently being shut down during MainViewModel.onCleared().

Changes:

  • Added ExecutorUtils.safeExecute(...) to skip submissions on shutdown pools and swallow RejectedExecutionException.
  • Routed QTH/grid lookup and TX-audio send submissions in MainViewModel through safeExecute.
  • Added unit tests validating safeExecute behavior for active/shutdown/null inputs.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
ft8af/app/src/main/java/com/k1af/ft8af/ExecutorUtils.java Introduces safeExecute helper to avoid crashes from executor teardown races.
ft8af/app/src/main/java/com/k1af/ft8af/MainViewModel.java Uses safeExecute for decode follow-up and TX-audio executor submissions.
ft8af/app/src/test/java/com/k1af/ft8af/ExecutorUtilsTest.java Adds unit tests for safeExecute behavior across pool/task states.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +35 to +46
@Test
public void shutdownPool_dropsTaskAndReturnsFalse() {
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.shutdown();
// The pre-fix code path: execute() on a terminated pool threw and crashed
// the decode thread. safeExecute must swallow it and report the drop.
boolean accepted = ExecutorUtils.safeExecute(pool, () -> {
throw new AssertionError("task must not run on a shut-down pool");
});
assertThat(accepted).isFalse();
}

@patrickrb

Copy link
Copy Markdown
Owner Author

Consolidated into #329 (single combined PR) as requested.

@patrickrb patrickrb closed this Jun 23, 2026
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