Skip to content

Commit

Permalink
Make parallel task dispatches respect dependency ordering
Browse files Browse the repository at this point in the history
  • Loading branch information
Technici4n committed May 2, 2024
1 parent b8d0e33 commit 7476536
Showing 1 changed file with 47 additions and 8 deletions.
55 changes: 47 additions & 8 deletions loader/src/main/java/net/neoforged/fml/ModLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -190,22 +191,58 @@ public static void waitForTask(String name, Runnable periodicTask, CompletableFu
}
}

/**
* Exception that is fired when a mod loading future cannot be executed because a dependent future failed.
* It is only used for control flow and easy filtering out, but never logged or propagated further.
*/
private static class DependentFutureFailedException extends RuntimeException {}

/**
* Dispatches a task across all mod containers in parallel, with progress displayed on the loading screen.
*/
public static void dispatchParallelTask(String name, Executor parallelExecutor, Runnable periodicTask, Consumer<ModContainer> task) {
var progress = StartupNotificationManager.addProgressBar(name, modList.size());
try {
periodicTask.run();
Map<IModInfo, CompletableFuture<Void>> modFutures = new IdentityHashMap<>();
var futureList = modList.getSortedMods().stream()
.map(modContainer -> {
return CompletableFuture.runAsync(() -> {
ModLoadingContext.get().setActiveContainer(modContainer);
task.accept(modContainer);
}, parallelExecutor).whenComplete((result, exception) -> {
progress.increment();
ModLoadingContext.get().setActiveContainer(null);
});
// Build future for all dependencies first
var deps = LoadingModList.get().getDependencies(modContainer.getModInfo());
@SuppressWarnings("unchecked")
CompletableFuture<Void>[] depFutures = new CompletableFuture[deps.size()];
for (int i = 0; i < deps.size(); ++i) {
depFutures[i] = modFutures.get(deps.get(i));
if (depFutures[i] == null) {
throw new IllegalStateException("Dependency future for mod %s which is a dependency of %s not found!".formatted(
deps.get(i).getModId(), modContainer.getModId()));
}
}

var combinedFuture = depFutures.length == 0 ? CompletableFuture.completedFuture(null) : CompletableFuture.allOf(depFutures);

// Build the future for this container
var future = combinedFuture.<Void>handleAsync((void_, exception) -> {
if (exception != null) {
// If there was any exception, short circuit.
// The exception will already be handled by `waitForFuture` since it comes from another mod.
LOGGER.error("Skipping {} future for mod {} because a dependency threw an exception.", name, modContainer.getModId());
progress.increment();
// Throw a marker exception to make sure that dependencies of *this* task don't get executed.
throw new DependentFutureFailedException();
}

try {
ModLoadingContext.get().setActiveContainer(modContainer);
task.accept(modContainer);
} finally {
progress.increment();
ModLoadingContext.get().setActiveContainer(null);
}
return null;
}, parallelExecutor);
modFutures.put(modContainer.getModInfo(), future);
return future;
})
.toList();
var singleFuture = ModList.gather(futureList)
Expand All @@ -226,7 +263,9 @@ private static void waitForFuture(String name, Runnable periodicTask, Completabl
// Merge all potential modloading issues
var errorCount = 0;
for (var error : e.getCause().getSuppressed()) {
if (error instanceof ModLoadingException modLoadingException) {
if (error instanceof DependentFutureFailedException) {
continue;
} else if (error instanceof ModLoadingException modLoadingException) {
loadingIssues.addAll(modLoadingException.getIssues());
} else {
loadingIssues.add(ModLoadingIssue.error("fml.modloading.uncaughterror", name).withCause(e));
Expand Down

0 comments on commit 7476536

Please sign in to comment.