修复异步任务 OOM 后界面假死并显示错误信息 (#6636)

This commit is contained in:
Apostrophe 2026-08-10 21:58:49 +08:00 committed by GitHub
parent 78b8d37891
commit a0d1fa8f38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 97 additions and 2 deletions

View file

@ -68,14 +68,25 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
else if (!settings.containsKey("forbid_success_message"))
Controllers.dialog(i18n("message.success"), null, MessageType.SUCCESS, () -> onEnd());
} else {
if (executor.getException() == null)
if (executor.getException() == null) {
onEnd();
return;
}
if (executor.getException() instanceof CancellationException) {
onEnd();
return;
}
if (executor.getException().getCause() instanceof OutOfMemoryError outOfMemoryError) {
try {
Controllers.dialog(StringUtils.getStackTrace(outOfMemoryError), null, MessageType.ERROR, () -> onEnd());
} catch (OutOfMemoryError ignored) {
onEnd();
}
return;
}
String appendix = StringUtils.getStackTrace(executor.getException());
if (settings.get(WizardProvider.FailureCallback.KEY) != null)
settings.get(WizardProvider.FailureCallback.KEY).onFail(settings, executor.getException(), () -> onEnd());

View file

@ -65,7 +65,10 @@ public final class AsyncTaskExecutor extends TaskExecutor {
return success;
})
.exceptionally(e -> {
Lang.handleUncaughtException(resolveException(e));
Throwable resolved = resolveException(e);
if (resolved instanceof OutOfMemoryError)
taskListeners.forEach(it -> it.onStop(false, this));
Lang.handleUncaughtException(resolved);
return false;
});
return this;
@ -190,6 +193,8 @@ public final class AsyncTaskExecutor extends TaskExecutor {
}
task.setState(Task.TaskState.FAILED);
} else if (resolved instanceof OutOfMemoryError e) {
handleOutOfMemoryError(task, e);
}
throw new CompletionException(resolved); // rethrow error
@ -301,6 +306,8 @@ public final class AsyncTaskExecutor extends TaskExecutor {
taskListeners.forEach(it -> it.onFailed(task, e));
task.setState(Task.TaskState.FAILED);
} else if (resolved instanceof OutOfMemoryError e) {
handleOutOfMemoryError(task, e);
}
throw new CompletionException(resolved); // rethrow error
@ -315,6 +322,16 @@ public final class AsyncTaskExecutor extends TaskExecutor {
}
}
/// Completes the failed task lifecycle while preserving the original error for the global handler.
private void handleOutOfMemoryError(Task<?> task, OutOfMemoryError error) {
Exception taskException = new Exception(error);
task.setException(taskException);
exception = taskException;
task.fireDoneEvent(this, true);
taskListeners.forEach(it -> it.onFailed(task, error));
task.setState(Task.TaskState.FAILED);
}
private void checkCancellation() {
if (isCancelled()) {
throw new CancellationException("Cancelled by user");

View file

@ -20,12 +20,15 @@ package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
@ -47,6 +50,70 @@ public class TaskTest {
assertInstanceOf(Error.class, throwable.get(), "Error has not been thrown to uncaught exception handler");
}
/// Verifies that OOM completes the task lifecycle and still reaches the global handler.
@Test
public void testOutOfMemoryErrorLifecycle() {
OutOfMemoryError normalTaskError = new OutOfMemoryError("normal task");
assertOutOfMemoryErrorLifecycle(Task.runAsync(() -> {
throw normalTaskError;
}), normalTaskError);
OutOfMemoryError futureTaskError = new OutOfMemoryError("completable future task");
CompletableFuture<Void> future = new CompletableFuture<>();
future.completeExceptionally(futureTaskError);
assertOutOfMemoryErrorLifecycle(Task.fromCompletableFuture(future), futureTaskError);
}
/// Checks the lifecycle events and recorded exceptions for one OOM task.
private void assertOutOfMemoryErrorLifecycle(Task<?> task, OutOfMemoryError error) {
AtomicInteger doneCount = new AtomicInteger();
AtomicReference<@Nullable Boolean> doneFailed = new AtomicReference<>();
AtomicInteger failedCount = new AtomicInteger();
AtomicReference<@Nullable Throwable> failedThrowable = new AtomicReference<>();
AtomicInteger stopCount = new AtomicInteger();
AtomicReference<@Nullable Boolean> stopSuccess = new AtomicReference<>();
AtomicReference<@Nullable Throwable> uncaught = new AtomicReference<>();
task.onDone().register(event -> {
doneCount.incrementAndGet();
doneFailed.set(event.isFailed());
});
TaskExecutor executor = task.executor(new TaskListener() {
@Override
public void onFailed(Task<?> failedTask, Throwable throwable) {
failedCount.incrementAndGet();
failedThrowable.set(throwable);
}
@Override
public void onStop(boolean success, TaskExecutor taskExecutor) {
stopCount.incrementAndGet();
stopSuccess.set(success);
}
});
@Nullable Thread.UncaughtExceptionHandler previousHandler = Thread.getDefaultUncaughtExceptionHandler();
try {
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> uncaught.set(throwable));
assertFalse(executor.test());
} finally {
Thread.setDefaultUncaughtExceptionHandler(previousHandler);
}
assertEquals(1, doneCount.get());
assertEquals(Boolean.TRUE, doneFailed.get());
assertEquals(1, failedCount.get());
assertSame(error, failedThrowable.get());
assertEquals(1, stopCount.get());
assertEquals(Boolean.FALSE, stopSuccess.get());
assertEquals(Task.TaskState.FAILED, task.getState());
assertNotNull(task.getException());
assertSame(error, task.getException().getCause());
assertNotNull(executor.getException());
assertSame(error, executor.getException().getCause());
assertSame(error, uncaught.get());
}
/**
*
*/