IuUtilityTaskController.java
/*
* Copyright © 2025 Indiana University
* All rights reserved.
*
* BSD 3-Clause License
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* - Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package edu.iu;
import java.time.Instant;
import java.util.Date;
import java.util.Optional;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Controller for executing <strong>utility tasks</strong>.
*
* <p>
* A <strong>utility task</strong> is timed, potentially blocking, and
* non-business critical. For example, establishing a database connection.
* <strong>Utility tasks</strong> are not required to complete before the JVM
* can exit.
* </p>
*
* <p>
* To ensure threads are tied to the lowest level parent group possible, the
* utility executor <em>should</em> be initialized by the main bootstrap method
* or closest reasonable equivalent, i.e., ServletListener, as in the example
* below.
* </p>
*
* <pre>
* Class.forName(IuUtilityWorkloadController.class.getName());
* </pre>
*
* <p>
* <strong>Implementation Note:</strong> The executor backing this workload
* controller is deliberately small and cannot be configured. <strong>Utility
* tasks</strong> <strong>should</strong> typically complete in 5ms or less
* under normal environment conditions, and only run into exhaustion issues when
* downstream resources slow down. The small size of the utility executor allow
* {@link RejectedExecutionException} provide fail-fast behavior as preferable
* to destabilizing the entire application.
* </p>
*
* @param <T> result type
*/
public class IuUtilityTaskController<T> implements UnsafeSupplier<T> {
private static final Timer TIMER;
private static final ExecutorService EXEC;
static {
final var threadGroup = new ThreadGroup("iu-java-util");
final var threadFactory = new ThreadFactory() {
private int c;
@Override
public Thread newThread(Runnable r) {
final var thread = new Thread(threadGroup, r, "iu-java-util/" + ++c);
thread.setDaemon(true);
return thread;
}
};
TIMER = new Timer("iu-java-util", true);
EXEC = new ThreadPoolExecutor(4, 16, 15L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(256, false),
threadFactory);
}
private final Instant expires;
private final TimerTask interrupt;
private volatile Thread thread;
private volatile Optional<T> result;
private volatile Throwable error;
/**
* Runs a <strong>utility task</strong>.
*
* @param task <strong>utility task</strong>
* @param expires {@link Instant} the task must be completed by
* @throws TimeoutException at {@code expires} if the task has not completed
* normally
* @throws Throwable if thrown from the task
*/
public static void doBefore(UnsafeRunnable task, Instant expires) throws TimeoutException, Throwable {
new IuUtilityTaskController<>(() -> {
task.run();
return null;
}, expires).get();
}
/**
* Gets a value from a <strong>utility factory</strong>.
*
* @param <T> value type
*
* @param factory <strong>utility factory</strong>
* @param expires {@link Instant} the value must be supplied by
* @return value
* @throws TimeoutException at {@code expires} if the value has not been
* supplied
* @throws Throwable if thrown from the factory
*/
public static <T> T getBefore(UnsafeSupplier<T> factory, Instant expires) throws TimeoutException, Throwable {
return new IuUtilityTaskController<>(factory, expires).get();
}
/**
* Creates a <strong>utility task</strong> controller.
*
* @param task {@link UnsafeSupplier} <strong>utility task</strong>
* @param expires {@link Instant} the task will expire
*/
public IuUtilityTaskController(UnsafeSupplier<T> task, Instant expires) {
this.expires = expires;
final var context = Thread.currentThread().getContextClassLoader();
interrupt = new TimerTask() {
@Override
public void run() {
synchronized (IuUtilityTaskController.this) {
thread.interrupt();
}
}
};
final var callerStackTrace = new Throwable("caller stack trace");
TIMER.schedule(interrupt, Date.from(expires.plusMillis(250L)));
EXEC.submit(new Runnable() {
@Override
public void run() {
thread = Thread.currentThread();
final var restoreContext = thread.getContextClassLoader();
try {
thread.setContextClassLoader(context);
result = Optional.ofNullable(task.get());
} catch (Throwable e) {
e.addSuppressed(callerStackTrace);
error = e;
} finally {
thread.setContextClassLoader(restoreContext);
interrupt.cancel();
synchronized (IuUtilityTaskController.this) {
thread = null;
IuUtilityTaskController.this.notifyAll();
}
}
}
});
}
@Override
public T get() throws Throwable {
IuObject.waitFor(this, () -> result != null || error != null, expires);
if (error != null)
throw error;
else
return result.orElse(null);
}
}