io.deephaven.engine.table.impl.util.JobScheduler Maven / Gradle / Ivy
Show all versions of deephaven-engine-table Show documentation
package io.deephaven.engine.table.impl.util;
import io.deephaven.base.log.LogOutput;
import io.deephaven.base.log.LogOutputAppendable;
import io.deephaven.base.verify.Assert;
import io.deephaven.engine.context.ExecutionContext;
import io.deephaven.engine.table.Context;
import io.deephaven.engine.table.impl.perf.BasePerformanceEntry;
import io.deephaven.io.log.impl.LogOutputStringImpl;
import io.deephaven.util.SafeCloseable;
import io.deephaven.util.annotations.FinalDefault;
import io.deephaven.util.process.ProcessEnvironment;
import io.deephaven.util.referencecounting.ReferenceCounted;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* An interface for submitting jobs to be executed. Submitted jobs may be executed on the current thread, or in separate
* threads (thus allowing true parallelism). Performance metrics are accumulated for all executions off the current
* thread for inclusion in overall task metrics.
*/
public interface JobScheduler {
/**
* A default context for the scheduled job actions. Override this to provide reusable resources for the serial and
* parallel iterate actions.
*/
interface JobThreadContext extends Context {
}
JobThreadContext DEFAULT_CONTEXT = new JobThreadContext() {};
Supplier DEFAULT_CONTEXT_FACTORY = () -> DEFAULT_CONTEXT;
/**
* Cause runnable to be executed.
*
* @param executionContext the execution context to run it under
* @param runnable the runnable to execute
* @param description a description for logging
* @param onError a routine to call if an exception occurs while running runnable
*/
void submit(
ExecutionContext executionContext,
Runnable runnable,
final LogOutputAppendable description,
final Consumer onError);
/**
* The performance statistics of all runnables that have been completed off-thread, or null if all were executed in
* the current thread.
*/
BasePerformanceEntry getAccumulatedPerformance();
/**
* How many threads exist in the job scheduler? The job submitters can use this value to determine how many sub-jobs
* to split work into.
*/
int threadCount();
/**
* Helper interface for {@code iterateSerial()} and {@code iterateParallel()}. This provides a functional interface
* with {@code index} indicating which iteration to perform. When this returns, the scheduler will automatically
* schedule the next iteration.
*/
@FunctionalInterface
interface IterateAction {
/**
* Iteration action to be invoked.
*
* @param taskThreadContext The context, unique to this task-thread
* @param index The iteration number
* @param nestedErrorConsumer A consumer to pass to directly-nested iterative jobs
*/
void run(CONTEXT_TYPE taskThreadContext, int index, Consumer nestedErrorConsumer);
}
/**
* Helper interface for {@link #iterateSerial} and {@link #iterateParallel}. This provides a functional interface
* with {@code index} indicating which iteration to perform and {@link Runnable resume} providing a mechanism to
* inform the scheduler that the current task is complete. When {@code resume} is called, the scheduler will
* automatically schedule the next iteration.
*
* NOTE: failing to call {@code resume} will result in the scheduler not scheduling all remaining iterations. This
* will not block the scheduler, but the {@code completeAction} {@link Runnable} will never be called.
*/
@FunctionalInterface
interface IterateResumeAction {
/**
* Iteration action to be invoked.
*
* @param taskThreadContext The context, unique to this task-thread
* @param index The iteration number
* @param nestedErrorConsumer A consumer to pass to directly-nested iterative jobs
* @param resume A function to call to move on to the next iteration
*/
void run(CONTEXT_TYPE taskThreadContext, int index, Consumer nestedErrorConsumer, Runnable resume);
}
final class IterationManager extends ReferenceCounted
implements LogOutputAppendable {
private static void onUnexpectedJobError(@NotNull final Exception exception) {
ProcessEnvironment.getGlobalFatalErrorReporter().report("Unexpected iteration job error", exception);
}
private final LogOutputAppendable description;
private final int start;
private final int count;
private final Consumer onError;
private final IterateResumeAction action;
private final Runnable onComplete;
private final AtomicInteger nextAvailableTaskIndex;
private final AtomicInteger remainingTaskCount;
private final AtomicReference exception;
IterationManager(
@Nullable final LogOutputAppendable description,
final int start,
final int count,
@NotNull final IterateResumeAction action,
@NotNull final Runnable onComplete,
@NotNull final Consumer onError) {
this.description = description;
this.start = start;
this.count = count;
this.onError = onError;
this.action = action;
this.onComplete = onComplete;
nextAvailableTaskIndex = new AtomicInteger(start);
remainingTaskCount = new AtomicInteger(count);
exception = new AtomicReference<>();
}
private void startTasks(
@NotNull final JobScheduler scheduler,
@Nullable final ExecutionContext executionContext,
@NotNull final Supplier taskThreadContextFactory,
final int maxThreads) {
// Increment this once in order to maintain >=1 until completed
incrementReferenceCount();
final int numTaskInvokers = Math.min(maxThreads, scheduler.threadCount());
for (int tii = 0; tii < numTaskInvokers; ++tii) {
final int initialTaskIndex = nextAvailableTaskIndex.getAndIncrement();
if (initialTaskIndex >= start + count || exception.get() != null) {
break;
}
final CONTEXT_TYPE context = taskThreadContextFactory.get();
if (!tryIncrementReferenceCount()) {
context.close();
break;
}
final TaskInvoker taskInvoker = new TaskInvoker(context, tii, initialTaskIndex);
scheduler.submit(executionContext, taskInvoker::execute, description,
IterationManager::onUnexpectedJobError);
}
}
private void onTaskComplete() {
if (remainingTaskCount.decrementAndGet() == 0) {
Assert.eqNull(exception.get(), "exception.get()");
decrementReferenceCount();
}
}
private void onTaskError(@NotNull final Exception e) {
if (exception.compareAndSet(null, e)) {
decrementReferenceCount();
}
}
@Override
protected void onReferenceCountAtZero() {
final Exception localException = exception.get();
if (localException != null) {
try {
onError.accept(localException);
} catch (Exception e) {
e.addSuppressed(localException);
onUnexpectedJobError(e);
}
return;
}
try {
onComplete.run();
} catch (Exception e) {
try {
onError.accept(e);
} catch (Exception e2) {
e2.addSuppressed(e);
onUnexpectedJobError(e2);
}
}
}
@Override
public LogOutput append(@NotNull final LogOutput logOutput) {
return logOutput.append(description)
.append("-IterationManager[start=").append(start)
.append(",count=").append(count)
.append(",nextAvailableTaskIndex=").append(nextAvailableTaskIndex.get())
.append(",remainingTaskCount=").append(remainingTaskCount.get())
.append(",exceptionSet=").append(exception.get() != null)
.append(']');
}
private class TaskInvoker implements LogOutputAppendable {
private final CONTEXT_TYPE context;
private final int invokerIndex;
private int acquiredTaskIndex;
private boolean closed;
private boolean running;
/**
* Construct a TaskInvoker which will iteratively reschedule itself to perform parallel tasks as needed.
* This constructor "transfers ownership" to a single reference count on the enclosing IterationManager to
* the result TaskInvoker, to be released on error or work exhaustion.
*
* @param context The context to be used for all tasks performed by this TaskInvoker
* @param invokerIndex The index of this TaskInvoker within the IterationManager, for debugging and logging
* purposes
* @param initialTaskIndex The index of the initial task to perform
*/
private TaskInvoker(
@NotNull final CONTEXT_TYPE context,
final int invokerIndex,
final int initialTaskIndex) {
this.context = context;
this.invokerIndex = invokerIndex;
acquiredTaskIndex = initialTaskIndex;
}
private synchronized void execute() {
int runningTaskIndex;
do {
if (exception.get() != null) {
// We acquired a task index, but the operation is aborting because some other thread reported
// an error.
close();
return;
}
runningTaskIndex = acquiredTaskIndex;
try {
running = true;
action.run(
context,
runningTaskIndex,
this::reportError,
this::reportTaskCompleteAndResumeIteration);
} catch (Exception e) {
if (closed) {
// The task threw an error while trying to deliver another error or complete the iteration.
// We cannot safely deliver this error, but we don't want to allow incorrect operation, so
// we report it to the global error reporter.
onUnexpectedJobError(e);
} else {
// Something went wrong, but no completion or error was delivered yet. Report the error.
reportError(e);
}
return;
} finally {
running = false;
}
} while (runningTaskIndex != acquiredTaskIndex && !closed);
}
private synchronized void reportTaskCompleteAndResumeIteration() {
// This might be called from the original thread that ran our action for acquiredTaskIndex, *or* from
// a thread that completed that task asynchronously. Regardless, we always try to acquire a new task,
// freeing our resources if there are no tasks remaining or an error was reported asynchronously.
// If we *do* have a task to execute, if we're on the original thread (running == true) we return in
// order to allow the enclosing loop to execute our task in an orderly fashion without any recursion
// in the thread stack, else we run it here, hijacking the thread that reported the prior iteration's
// completion.
onTaskComplete();
if ((acquiredTaskIndex = nextAvailableTaskIndex.getAndIncrement()) >= start + count
|| exception.get() != null) {
close();
} else if (!running) {
execute();
}
}
private synchronized void reportError(@NotNull final Exception e) {
try (final SafeCloseable ignored = this::close) {
onTaskError(Objects.requireNonNull(e));
}
}
private void close() {
Assert.eqFalse(closed, "closed");
try (final SafeCloseable ignored = context) {
closed = true;
} finally {
decrementReferenceCount();
}
}
@Override
public LogOutput append(@NotNull final LogOutput logOutput) {
return logOutput.append(IterationManager.this)
.append("-TaskInvoker[invokerIndex=").append(invokerIndex)
.append(",acquiredTaskIndex=").append(acquiredTaskIndex)
.append(",closed=").append(closed)
.append(']');
}
@Override
public String toString() {
return new LogOutputStringImpl().append(this).toString();
}
}
}
/**
* Provides a mechanism to iterate over a range of values in parallel using the {@link JobScheduler}
*
* @param executionContext the execution context for this task
* @param description the description to use for logging
* @param taskThreadContextFactory the factory that supplies {@link JobThreadContext contexts} for the threads
* handling the sub-tasks
* @param start the integer value from which to start iterating
* @param count the number of times this task should be called
* @param action the task to perform, the current iteration index is provided as a parameter
* @param onComplete this will be called when all iterations are complete
* @param onError error handler for the scheduler to use while iterating
*/
@FinalDefault
default void iterateParallel(
@Nullable final ExecutionContext executionContext,
@Nullable final LogOutputAppendable description,
@NotNull final Supplier taskThreadContextFactory,
final int start,
final int count,
@NotNull final IterateAction action,
@NotNull final Runnable onComplete,
@NotNull final Consumer onError) {
iterateParallel(executionContext, description, taskThreadContextFactory, start, count,
(final CONTEXT_TYPE taskThreadContext,
final int taskIndex,
final Consumer nestedErrorConsumer,
final Runnable resume) -> {
action.run(taskThreadContext, taskIndex, nestedErrorConsumer);
resume.run();
},
onComplete, onError);
}
/**
* Provides a mechanism to iterate over a range of values in parallel using the {@link JobScheduler}. The advantage
* to using this over the other method is the resumption callable on {@code action} that will trigger the next
* execution. This allows the next iteration and the completion runnable to be delayed until dependent asynchronous
* serial or parallel scheduler jobs have completed.
*
* @param executionContext the execution context for this task
* @param description the description to use for logging
* @param taskThreadContextFactory the factory that supplies {@link JobThreadContext contexts} for the tasks
* @param start the integer value from which to start iterating
* @param count the number of times this task should be called
* @param action the task to perform, the current iteration index and a resume Runnable are parameters
* @param onComplete this will be called when all iterations are complete
* @param onError error handler for the scheduler to use while iterating
*/
@FinalDefault
default void iterateParallel(
@Nullable final ExecutionContext executionContext,
@Nullable final LogOutputAppendable description,
@NotNull final Supplier taskThreadContextFactory,
final int start,
final int count,
@NotNull final IterateResumeAction action,
@NotNull final Runnable onComplete,
@NotNull final Consumer onError) {
if (count == 0) {
// no work to do
onComplete.run();
}
final IterationManager iterationManager =
new IterationManager<>(description, start, count, action, onComplete, onError);
iterationManager.startTasks(this, executionContext, taskThreadContextFactory, count);
}
/**
* Provides a mechanism to iterate over a range of values serially using the {@link JobScheduler}. The advantage to
* using this over a simple iteration is the resumption callable on {@code action} that will trigger the next
* execution. This allows the next iteration and the completion runnable to be delayed until dependent asynchronous
* serial or parallel scheduler jobs have completed.
*
* @param executionContext the execution context for this task
* @param description the description to use for logging
* @param taskThreadContextFactory the factory that supplies {@link JobThreadContext contexts} for the tasks
* @param start the integer value from which to start iterating
* @param count the number of times this task should be called
* @param action the task to perform, the current iteration index and a resume Runnable are parameters
* @param onComplete this will be called when all iterations are complete
* @param onError error handler for the scheduler to use while iterating
*/
@FinalDefault
default void iterateSerial(
@Nullable final ExecutionContext executionContext,
@Nullable final LogOutputAppendable description,
@NotNull final Supplier taskThreadContextFactory,
final int start,
final int count,
@NotNull final IterateResumeAction action,
@NotNull final Runnable onComplete,
@NotNull final Consumer onError) {
if (count == 0) {
// no work to do
onComplete.run();
}
final IterationManager iterationManager =
new IterationManager<>(description, start, count, action, onComplete, onError);
iterationManager.startTasks(this, executionContext, taskThreadContextFactory, 1);
}
}