-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #261 from ascopes/task/refactor-executor
Refactor executor out into separate class that is individually testable
- Loading branch information
Showing
6 changed files
with
510 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
...-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/utils/ConcurrentExecutor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/* | ||
* Copyright (C) 2023 - 2024, Ashley Scopes. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package io.github.ascopes.protobufmavenplugin.utils; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.concurrent.Callable; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.FutureTask; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.stream.Collector; | ||
import java.util.stream.Collectors; | ||
import javax.annotation.PreDestroy; | ||
import javax.inject.Named; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* Helper component that allows scheduling IO-bound tasks within a thread pool. | ||
* | ||
* @author Ashley Scopes | ||
* @since 2.2.0 | ||
*/ | ||
@Named | ||
public final class ConcurrentExecutor { | ||
|
||
private static final Logger log = LoggerFactory.getLogger(ConcurrentExecutor.class); | ||
private final ExecutorService executorService; | ||
|
||
public ConcurrentExecutor() { | ||
ExecutorService executorService; | ||
|
||
try { | ||
log.debug("Trying to create new Loom virtual thread pool"); | ||
executorService = (ExecutorService) Executors.class | ||
.getMethod("newVirtualThreadPerTaskExecutor") | ||
.invoke(null); | ||
|
||
log.debug("Loom virtual thread pool creation was successful!"); | ||
|
||
} catch (Exception ex) { | ||
var concurrency = Runtime.getRuntime().availableProcessors() * 8; | ||
log.debug( | ||
"Falling back to new work-stealing thread pool (concurrency={}, Loom is unavailable)", | ||
concurrency | ||
); | ||
executorService = Executors.newWorkStealingPool(concurrency); | ||
} | ||
|
||
this.executorService = executorService; | ||
} | ||
|
||
/** | ||
* Destroy the internal thread pool. | ||
* | ||
* @throws InterruptedException if destruction timed out or the thread was interrupted. | ||
*/ | ||
@PreDestroy | ||
@SuppressWarnings({"ResultOfMethodCallIgnored", "unused"}) | ||
public void destroy() throws InterruptedException { | ||
log.debug("Shutting down executor..."); | ||
executorService.shutdown(); | ||
log.debug("Awaiting executor termination..."); | ||
|
||
// If this fails, then we can't do much about it. Force shutdown and hope threads don't | ||
// deadlock. Not going to bother adding complicated handling here as if we get stuck, we | ||
// likely have far bigger problems to deal with. | ||
executorService.awaitTermination(10, TimeUnit.SECONDS); | ||
var remaining = executorService.shutdownNow(); | ||
log.debug("Shutdown ended, stubborn remaining tasks that will be orphaned: {}", remaining); | ||
} | ||
|
||
public <R> FutureTask<R> submit(Callable<R> task) { | ||
var futureTask = new FutureTask<>(task); | ||
executorService.submit(futureTask); | ||
return futureTask; | ||
} | ||
|
||
/** | ||
* Return a reactive collector of all the results of a stream of scheduled tasks. | ||
* | ||
* @param <R> the task return type. | ||
* @return the collector. | ||
* @throws MultipleFailuresException if any of the results raised exceptions. All results are | ||
* collected prior to this being raised. | ||
*/ | ||
public <R> Collector<FutureTask<R>, ?, List<R>> awaiting() { | ||
return Collectors.collectingAndThen(Collectors.toUnmodifiableList(), this::await); | ||
} | ||
|
||
private <R> List<R> await(List<FutureTask<R>> scheduledTasks) { | ||
try { | ||
var results = new ArrayList<R>(); | ||
var exceptions = new ArrayList<Throwable>(); | ||
|
||
for (var task : scheduledTasks) { | ||
try { | ||
results.add(task.get()); | ||
} catch (ExecutionException ex) { | ||
exceptions.add(ex.getCause()); | ||
} catch (InterruptedException ex) { | ||
Thread.currentThread().interrupt(); | ||
return results; | ||
} | ||
} | ||
|
||
if (!exceptions.isEmpty()) { | ||
throw MultipleFailuresException.create(exceptions); | ||
} | ||
|
||
return results; | ||
|
||
} finally { | ||
// Interrupt anything that didn't complete if we get interrupted on the OS level. | ||
for (var task : scheduledTasks) { | ||
task.cancel(true); | ||
} | ||
} | ||
} | ||
|
||
} |
47 changes: 47 additions & 0 deletions
47
.../src/main/java/io/github/ascopes/protobufmavenplugin/utils/MultipleFailuresException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/* | ||
* Copyright (C) 2023 - 2024, Ashley Scopes. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package io.github.ascopes.protobufmavenplugin.utils; | ||
|
||
import java.util.List; | ||
import java.util.NoSuchElementException; | ||
|
||
/** | ||
* Exception that gets raised when one or more concurrent tasks fail. | ||
* | ||
* @author Ashley Scopes | ||
* @since 2.2.0 | ||
*/ | ||
public final class MultipleFailuresException extends RuntimeException { | ||
|
||
private MultipleFailuresException(String message, Throwable cause) { | ||
super(message, cause, true, false); | ||
} | ||
|
||
/** | ||
* Initialise this exception. | ||
* | ||
* @param exceptions the exceptions that were thrown. Must have at least one item. | ||
* @return the wrapper exception. | ||
* @throws NoSuchElementException if an empty list was provided. | ||
*/ | ||
public static MultipleFailuresException create(List<? extends Throwable> exceptions) { | ||
var causeIterator = exceptions.iterator(); | ||
var ex = new MultipleFailuresException("Multiple failures occurred", causeIterator.next()); | ||
causeIterator.forEachRemaining(ex::addSuppressed); | ||
return ex; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.