/*
* Strongback
* Copyright 2015, Strongback and individual contributors by the @authors tag.
* See the COPYRIGHT.txt in the distribution for a full listing of individual
* contributors.
*
* Licensed under the MIT License; you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* 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 org.strongback;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.LongConsumer;
import java.util.function.Supplier;
import org.strongback.AsyncEventRecorder.EventWriter;
import org.strongback.Logger.Level;
import org.strongback.annotation.NotImplemented;
import org.strongback.annotation.ThreadSafe;
import org.strongback.command.Command;
import org.strongback.command.CommandState;
import org.strongback.command.Scheduler;
import org.strongback.command.Scheduler.CommandListener;
import org.strongback.components.Clock;
import org.strongback.components.Counter;
import org.strongback.components.Switch;
import org.strongback.util.Metronome;
/**
* Access point for a number of the higher-level Strongback functions. This class can be used within robot code or within unit
* tests.
*
* <h2>Configuration</h2>
* <p>
* Strongback will by default use the system logger, FPGA time (if available), and an executor that operates on a 5 millisecond
* execution period. If these defaults are not acceptable, then the Strongback library needs to be configured programmatically
* before you use it.
* <p>
* To configure Strongback, do the following once in the initialization of your robot (perhaps very early in the
* {@link edu.wpi.first.wpilibj.IterativeRobot#robotInit()} method):
* <ol>
* <li>call the {@link #configure()} method to obtain the {@link Configurator} instance,</li>
* <li>call any combination of the "use" or "set" methods on the {@link Configurator} instance,</li>
* <li>call the {@link Configurator#initialize() initialize()} method on the {@link Configurator} instance.</li>
* </ol>
* After that, the configuration should not be adjusted again, and any of the other Strongback methods can be used.
* <p>
* For example, the following code configures Strongback to use what happen to be the default logger, time system, 5ms executor
* (that uses busy-wait loops rather than {@link Thread#sleep(long)}), and automatically recording data and events to files on
* the RoboRIO:
*
* <pre>
* Strongback.configure()
* .useSystemLogger(Logger.Level.INFO)
* .useFpgaTime()
* .useExecutionPeriod(5, TimeUnit.MILLISECONDS)
* .useExecutionWaitMode(WaitMode.BUSY)
* .initialize();
* // Strongback is ready to use ...
* </pre>
*
* @author Randall Hauch
*/
@ThreadSafe
public final class Strongback {
public static final class Configurator {
public static enum TimerMode {
/**
* The thread uses a busy loop to prevent context switching to accurately wait for the prescribed amount of time.
* This is a very accurate approach, but the thread remains busy the entire time. See
* {@link Metronome#busy(long, TimeUnit, Clock)} for details.
*/
BUSY, /**
* The thread uses {@link Thread#sleep(long)} to wait for the prescribed amount of time. This may not be very
* accurate, but it is efficient since the thread will pause so that other work can be done by other threads.
* See {@link Metronome#sleeper(long, TimeUnit, Clock)} for details.
*/
SLEEP, /**
* The thread uses {@link LockSupport#parkNanos(long)} to wait for the prescribed amount of time. The
* accuracy of this approach will depend a great deal upon the hardware and operating system. See
* {@link Metronome#parker(long, TimeUnit, Clock)} for details.
*/
PARK;
}
private Supplier<Function<String, Logger>> loggersSupplier = () -> str -> new SystemLogger().enable(Level.INFO);
private Supplier<Clock> timeSystemSupplier = Clock::fpgaOrSystem;
private TimerMode executionWaitMode = TimerMode.BUSY;
private long executionPeriodInNanos = TimeUnit.MILLISECONDS.toNanos(20);
private volatile boolean initialized = false;
private String dataRecorderFilenameRoot = "strongback";
private String eventRecorderFilenameRoot = "strongback";
private int estimatedRecordDurationInSeconds = 180; // 3 minutes by default
private long eventRecordFileSizeInBytes = 1024 * 1024 * 2; // 2 MB by default
private boolean recordCommandStateChanges = true;
private Function<Iterable<DataRecorderChannel>, DataWriter> dataWriterFactory = this::createFileDataWriter;
private Supplier<EventWriter> eventWriterFactory = this::createFileEventWriter;
private LongConsumer excessiveExecutorDelayHandler = null;
private Supplier<String> dataRecorderFilenameGenerator = new Supplier<String>() {
private Counter counter = Counter.unlimited(1);
@Override
public String get() {
return dataRecorderFilenameRoot + "-data-" + counter.get() + ".dat";
}
};
private Supplier<String> eventRecorderFilenameGenerator = new Supplier<String>() {
private Counter counter = Counter.unlimited(1);
@Override
public String get() {
return eventRecorderFilenameRoot + "-event-" + counter.get() + ".dat";
}
};
protected DataWriter createFileDataWriter(Iterable<DataRecorderChannel> channels) {
int writesPerSecond = (int) (((double) TimeUnit.SECONDS.toNanos(1)) / executionPeriodInNanos);
return new FileDataWriter(channels, dataRecorderFilenameGenerator, writesPerSecond,
estimatedRecordDurationInSeconds);
}
protected EventWriter createFileEventWriter() {
return new FileEventWriter(eventRecorderFilenameGenerator, eventRecordFileSizeInBytes);
}
protected DataWriter createNetworkDataWriter(List<DataRecorderChannel> channels) {
return null;
}
/**
* Log messages to {@link SystemLogger System.out} at the specified level
*
* @param level the global logging level; may not be null
* @return this configurator so that methods can be chained together; never null
*/
public Configurator useSystemLogger(Logger.Level level) {
if (level == null) throw new IllegalArgumentException("The system logging level may not be null");
loggersSupplier = () -> (context) -> new SystemLogger().enable(level);
return this;
}
/**
* Log messages to custom {@link Logger} implementations based upon the supplied function that maps the string contexts
* to custom loggers.
*
* @param loggers the custom function that produces a logger for a context; may not be null
* @return this configurator so that methods can be chained together; never null
*/
public Configurator useCustomLogger(Function<String, Logger> loggers) {
if (loggers == null) throw new IllegalArgumentException("The custom loggers function may not be null");
loggersSupplier = () -> loggers;
return this;
}
/**
* Determine the time using the RoboRIO's FPGA's hardware if available, or the system time if FPGA hardware is not
* available.
*
* @return this configurator so that methods can be chained together; never null
*/
public Configurator useFpgaTime() {
timeSystemSupplier = Clock::fpgaOrSystem;
return this;
}
/**
* Determine the time using the JRE's {@link Clock#system() time system}.
*
* @return this configurator so that methods can be chained together; never null
*/
public Configurator useSystemTime() {
timeSystemSupplier = Clock::system;
return this;
}
/**
* Determine the time using a custom {@link Clock} implementation.
*
* @param clock the custom time system; may not be null
* @return this configurator so that methods can be chained together; never null
*/
public Configurator useCustomTime(Clock clock) {
if (clock == null) throw new IllegalArgumentException("The custom time system may not be null");
timeSystemSupplier = () -> clock;
return this;
}
/**
* Turn off the data recorder so that it does not record anything.
*
* @return this configurator so that methods can be chained together; never null
*/
public Configurator recordNoData() {
dataWriterFactory = null;
return this;
}
/**
* Record data to local files that begin with the given prefix.
*
* @param filenamePrefix the prefix for filenames; may not be null
* @return this configurator so that methods can be chained together; never null
*/
public Configurator recordDataToFile(String filenamePrefix) {
if (filenamePrefix == null) throw new IllegalArgumentException("The filename prefix may not be null");
dataRecorderFilenameRoot = filenamePrefix;
dataWriterFactory = this::createFileDataWriter;
return this;
}
/**
* Record data to the network tables.
*
* @return this configurator so that methods can be chained together; never null
*/
@NotImplemented
public Configurator recordDataToNetworkTables() {
throw new UnsupportedOperationException("Network data writer is not yet implemented");
// dataWriterFactory = this::createNetworkDataWriter;
// return this;
}
/**
* Record data to a custom {@link DataWriter} by supplying the factory that will create the data writer.
*
* @param customWriterFactory the factory for the {@link DataWriter} instance; may not be null
* @return this configurator so that methods can be chained together; never null
*/
public Configurator recordDataTo(Function<Iterable<DataRecorderChannel>, DataWriter> customWriterFactory) {
if (customWriterFactory == null) throw new IllegalArgumentException("The custom writer factory cannot be null");
dataWriterFactory = customWriterFactory;
return this;
}
/**
* Set the estimated number of seconds that the data recorder will capture. This is used to estimate by the data
* recorder to optimize any resources it uses.
*
* @param numberOfSeconds the estimated number of seconds of recorded data; must be non-negative
* @return this configurator so that methods can be chained together; never null
*/
public Configurator recordDuration(int numberOfSeconds) {
if (numberOfSeconds < 0) throw new IllegalArgumentException("The number of seconds may not be negative");
estimatedRecordDurationInSeconds = numberOfSeconds;
return this;
}
/**
* Record events to local files that begin with the given prefix.
*
* @param filenamePrefix the prefix for filenames; may not be null
* @param sizeInBytes the size of the files in bytes; must be at least 1024 bytes
* @return this configurator so that methods can be chained together; never null
*/
public Configurator recordEventsToFile(String filenamePrefix, long sizeInBytes) {
if (filenamePrefix == null) throw new IllegalArgumentException("The filename prefix may not be null");
if (sizeInBytes < 1024) throw new IllegalArgumentException("The event file size must be at least 1024 bytes");
eventRecorderFilenameRoot = filenamePrefix;
eventRecordFileSizeInBytes = sizeInBytes;
eventWriterFactory = this::createFileEventWriter;
return this;
}
/**
* Turn off the event recorder so that it does not record anything.
*
* @return this configurator so that methods can be chained together; never null
*/
public Configurator recordNoEvents() {
eventWriterFactory = null;
return this;
}
/**
* Automatically record all command state transitions to the event recorder.
*
* @return this configurator so that methods can be chained together; never null
*/
public Configurator recordCommands() {
recordCommandStateChanges = true;
return this;
}
/**
* Do not record any command state transitions to the event recorder.
*
* @return this configurator so that methods can be chained together; never null
*/
public Configurator recordNoCommands() {
recordCommandStateChanges = true;
return this;
}
/**
* Use the specified wait mode for Strongback's {@link Strongback#executor() executor}. This wait mode determines
* whether the executor's thread loops, sleeps, or parks until the {@link #useExecutionPeriod(long, TimeUnit) period}
* has elapsed.
*
* @param mode the desired wait mode; may not be null
* @return this configurator so that methods can be chained together; never null
* @see #useExecutionPeriod(long, TimeUnit)
*/
public Configurator useExecutionTimerMode(TimerMode mode) {
if (mode == null) throw new IllegalArgumentException("The execution timer mode may not be null");
executionWaitMode = mode;
return this;
}
/**
* Use the specified execution rate for Strongback's {@link Strongback#executor() executor}. The default execution rate
* is 5 milliseconds.
* <p>
* The clock that Strongback is configured to use will also affect the precision of the execution rate: the
* {@link #useFpgaTime() FPGA clock} will likely support rates down to around a few milliseconds, whereas the
* {@link #useSystemTime() system clock} may only support rates of 10-15 milliseconds. Therefore, this method does not
* currently allow sub-microsecond intervals.
*
* @param interval the interval for calling all registered {@link Executable}s; must be positive
* @param unit the time unit for the interval; may not be null
* @return this configurator so that methods can be chained together; never null
* @see #useExecutionTimerMode(TimerMode)
* @throws IllegalArgumentException if {@code unit} is {@link TimeUnit#MICROSECONDS} or {@link TimeUnit#NANOSECONDS}
*/
public Configurator useExecutionPeriod(long interval, TimeUnit unit) {
if (interval <= 0) throw new IllegalArgumentException("The execution interval must be positive");
if (unit == null) throw new IllegalArgumentException("The time unit may not be null");
if (TimeUnit.MILLISECONDS.toNanos(1) > unit.toNanos(interval)) {
throw new IllegalArgumentException("The interval must be at least 1 millisecond");
}
executionPeriodInNanos = unit.toNanos(interval);
return this;
}
/**
* Every time the executor takes longer than the {@link #useExecutionPeriod(long, TimeUnit) execution period} to execute
* each interval, report this to the given handler.
*
* @param handler the receiver for notifications of excessive execution times
* @return this configurator so that methods can be chained together; never null
*/
public Configurator reportExcessiveExecutionPeriods(LongConsumer handler) {
excessiveExecutorDelayHandler = handler;
return this;
}
/**
* When the supplied condition is {@code true}, call the supplied function with this Configurator.
* @param condition the condition that determines whether the supplied function should be called; may not be null
* @param configure the function that will perform additional configuration
* @return this configurator so that methods can be chained together; never null
*/
public Configurator when( boolean condition, Runnable configure ) {
return when(()->condition,configure);
}
/**
* When the supplied condition is {@code true}, call the supplied function with this Configurator.
* @param condition the function that determines whether the supplied function should be called; may not be null
* @param configure the function that will perform additional configuration
* @return this configurator so that methods can be chained together; never null
*/
public Configurator when( BooleanSupplier condition, Runnable configure ) {
if ( condition != null && configure != null && condition.getAsBoolean() ) {
configure.run();
}
return this;
}
/**
* When the supplied condition is {@code true}, call the supplied function with this Configurator.
* @param condition the condition that determines whether the supplied function should be called; may not be null
* @param configure the function that will perform additional configuration
* @return this configurator so that methods can be chained together; never null
*/
public Configurator when( boolean condition, Consumer<Configurator> configure ) {
return when(()->condition,configure);
}
/**
* When the supplied condition is {@code true}, call the supplied function with this Configurator.
* @param condition the function that determines whether the supplied function should be called; may not be null
* @param configure the function that will perform additional configuration
* @return this configurator so that methods can be chained together; never null
*/
public Configurator when( BooleanSupplier condition, Consumer<Configurator> configure ) {
if ( condition != null && configure != null && condition.getAsBoolean() ) {
configure.accept(this);
}
return this;
}
/**
* Complete the Strongback configuration and initialize Strongback so that it can be used.
*/
public synchronized void initialize() {
if (initialized) {
loggersSupplier.get()
.apply("")
.warn("Strongback has already been initialized. Make sure you configure and initialize Strongback only once");
}
initialized = true;
INSTANCE = new Strongback(this, Strongback.INSTANCE);
}
}
private static final Configurator CONFIG = new Configurator();
private static volatile Strongback INSTANCE = new Strongback(CONFIG, null);
/**
* Get the Strongback library configurator. Any configuration changes will take effect only after the
* {@link Configurator#initialize()} method is called.
*
* @return the configuration; never null
*/
public static Configurator configure() {
return CONFIG;
}
/**
* Start the Strongback functions, including the {@link #executor() Executor}, {@link #submit(Command) command scheduler},
* and the {@link #dataRecorder() data recorder}. This does nothing if Strongback is already started.
* <p>
* This is often useful to call in {@code IterativeRobot.autonomousInit()} to start Strongback and prepare for any
* autonomous based commands and start recording data and events.
*
* @see #restart()
*/
public static void start() {
INSTANCE.doStart();
}
/**
* Ensure that Strongback is {@link #start() started} and, if it was already running, {@link #killAllCommands() kill all
* currently-running commands}. It is equivalent to calling both {@code #start()} <em>and</em> {@code #killAllCommands()},
* although it is a bit more efficient.
* <p>
* This is often useful to use in {@code IterativeRobot.teleopInit()} to ensure Strongback is running and to cancel any
* commands that might still be running from autonomous mode.
*
* @see #start
* @see #killAllCommands()
*/
public static void restart() {
INSTANCE.doRestart();
}
/**
* Stop all currently-scheduled activity and flush all recorders. This is typically called by robot code when when the robot
* becomes disabled. Should the robot re-enable, all aspects of Strongback will continue to work as before it was disabled.
*/
public static void disable() {
INSTANCE.killCommandsAndFlush();
}
public static void shutdown() {
INSTANCE.doShutdown();
}
/**
* Get Strongback's automatically-configured {@link Executor} that repeatedly and efficiently performs asynchronous work on
* a precise interval using a single separate thread. Multiple {@link Executable}s can be registered with this executor, and
* doing so ensures that all of those {@link Executable}s are run on the same thread. This is more efficient than using
* multiple {@link Executor} instances, which each require their own thread.
* <p>
* Strongback's {@link #dataRecorder() data recorder}, {@link #switchReactor() switch reactor}, and {@link #submit(Command)
* internal scheduler} are already registered with this internal Executor, and therefore all use this single thread
* efficiently for all asynchronous processing.
* <p>
* However, care must be taken to prevent overloading the executor. Specifically, the executor must be able to perform all
* work for all registered {@link Executable}s during the {@link Configurator#useExecutionPeriod(long, TimeUnit) configured
* execution interval}. If too much work is added, the executor may fall behind.
*
* @return Strongback's executor; never null
* @see Configurator#useExecutionPeriod(long, TimeUnit)
* @see Configurator#useExecutionTimerMode(org.strongback.Strongback.Configurator.TimerMode)
*/
public static Executor executor() {
return INSTANCE.executables;
}
/**
* Get Strongback's global {@link Logger} implementation.
*
* @return Strongback's logger instance; never null
* @see Configurator#useSystemLogger(org.strongback.Logger.Level)
* @see Configurator#useCustomLogger(Function)
*/
public static Logger logger() {
return logger("");
}
/**
* Get Strongback's global {@link Logger} implementation.
*
* @param context the context of the logger
* @return Strongback's logger instance; never null
* @see Configurator#useSystemLogger(org.strongback.Logger.Level)
* @see Configurator#useCustomLogger(Function)
*/
public static Logger logger(String context) {
return INSTANCE.loggers.apply(context);
}
/**
* Get Strongback's global {@link Logger} implementation.
*
* @param context the context of the logger
* @return Strongback's logger instance; never null
* @see Configurator#useSystemLogger(org.strongback.Logger.Level)
* @see Configurator#useCustomLogger(Function)
*/
public static Logger logger(Class<?> context) {
return INSTANCE.loggers.apply(context.getName());
}
/**
* Get Strongback's {@link Clock time system} implementation.
*
* @return Strongback's time system instance; never null
* @see Configurator#useFpgaTime()
* @see Configurator#useSystemTime()
* @see Configurator#useCustomTime(Clock)
*/
public static Clock timeSystem() {
return INSTANCE.clock;
}
/**
* Submit a {@link Command} to be executed by Strongback's internal scheduler.
*
* @param command the command to be submitted
* @see Configurator#useExecutionPeriod(long, TimeUnit)
* @see Configurator#useExecutionTimerMode(org.strongback.Strongback.Configurator.TimerMode)
*/
public static void submit(Command command) {
if (command != null) INSTANCE.scheduler.submit(command);
}
/**
* Submit to Strongback's internal scheduler a {@link Command} that runs the supplied function one time and completes
* immediately.
*
* @param executeFunction the function to be called during execution; may not be null
*/
public static void submit(Runnable executeFunction) {
submit(Command.create(executeFunction));
}
/**
* Submit to Strongback's internal scheduler a {@link Command} that runs the supplied function one time, waits the
* prescribed amount of time, and then calls the second function.
*
* @param first the first function to be called; may not be null
* @param delayInSeconds the delay in seconds after the first function completes; must be positive
* @param second the second function to be called after the delay; may be null if not needed
*/
public static void submit(Runnable first, double delayInSeconds, Runnable second) {
submit(Command.create(delayInSeconds, first, second));
}
/**
* Submit to Strongback's internal scheduler a {@link Command} that runs the supplied function one or more times until it
* returns <code>false</code> or until the prescribed maximum time has passed, whichever comes first.
*
* @param function the function to be called at least one time and that should return <code>true</code> if it is to be
* called again; may not be null
* @param maxDurationInSeconds the maximum amount of time that the first function should be repeatedly called; must be
* positive
*/
public static void submit(BooleanSupplier function, double maxDurationInSeconds) {
submit(Command.create(maxDurationInSeconds, function));
}
/**
* Submit to Strongback's internal scheduler a {@link Command} that runs the supplied function one or more times until it
* returns <code>false</code> or until the prescribed maximum time has passed, and then calls the second function.
*
* @param first the first function to be called at least one time and that should return <code>true</code> if it is to be
* called again; may not be null
* @param maxDurationInSeconds the maximum amount of time that the first function should be repeatedly called; must be
* positive
* @param second the second function to be called after the delay; may be null if not needed
*/
public static void submit(BooleanSupplier first, double maxDurationInSeconds, Runnable second) {
submit(Command.create(maxDurationInSeconds, first, second));
}
/**
* Kill all currently-running commands.
*/
public static void killAllCommands() {
INSTANCE.scheduler.killAll();
}
/**
* Flush all data that has been recorded but not written to disk.
*/
public static void flushRecorders() {
INSTANCE.dataRecorderDriver.flush();
}
/**
* Get Strongback's {@link SwitchReactor} that can be used to call functions when {@link Switch switches} change state or
* while they remain in a specific state. The switch reactor is registered with the {@link #executor() executor}, so it
* periodically polls the registered switches and, based upon the current and previous states invokes the appropriate
* registered functions.
* <p>
* This is a great way to perform some custom logic based upon {@link Switch} states. For example, you could submit a
* specific command every time a button is pressed, or submit a command when a button is released, or run a command while a
* button is pressed. See {@link SwitchReactor} for details.
*
* @return the switch reactor; never null
* @see SwitchReactor
* @see Configurator#useExecutionPeriod(long, TimeUnit)
*/
public static SwitchReactor switchReactor() {
return INSTANCE.switchReactor;
}
/**
* Get Strongback's {@link DataRecorder} that can be used to register switches, motors, and other functions that provide
* recordable data. Once data providers have been registered, Strongback will only begin recording data after Strongback is
* {@link #start() started}, at which time the data recorder will automatically and repeatedly poll the data providers and
* write out the information to its log. Strongback should be {@link #disable() disabled} when the robot is disabled to
* flush any unwritten data and prevent recording data while in disabled mode. When the robot is enabled, it should again be
* started.
*
* @return the data recorder; never null
* @see DataRecorder
* @see Configurator#recordDataTo(Function)
* @see Configurator#recordDataToFile(String)
* @see Configurator#recordDataToNetworkTables()
* @see Configurator#recordDuration(int)
* @see Configurator#recordNoData()
*/
public static DataRecorder dataRecorder() {
return INSTANCE.dataRecorderChannels;
}
/**
* Get Strongback's {@link EventRecorder} used to record non-regular events and activities. If Strongback is configured to
* {@link Configurator#recordCommands() automatically record commands}, then all changes to command states are recorded in
* this event log. However, custom code can also explicitly {@link EventRecorder#record(String, String) record events} to
* the same log.
*
* @return the event recorder
* @see EventRecorder
* @see Configurator#recordCommands()
* @see Configurator#recordNoCommands()
* @see Configurator#recordEventsToFile(String, long)
* @see Configurator#recordNoEvents()
*/
public static EventRecorder eventRecorder() {
return INSTANCE.eventRecorder;
}
/**
* Get the number of times the {@link #executor() executor} has been unable to execute all work within the time period
* {@link Configurator#useExecutionPeriod(long, TimeUnit) specified in the configuration}.
*
* @return the number of excessive delays
*/
public static long excessiveExecutionTimeCounts() {
return INSTANCE.executorDelayCounter.get();
}
private final Function<String, Logger> loggers;
private final Executables executables;
private final ExecutorDriver executorDriver;
private final Clock clock;
private final Metronome metronome;
private final Scheduler scheduler;
private final AsyncSwitchReactor switchReactor;
private final DataRecorderChannels dataRecorderChannels;
private final DataRecorderDriver dataRecorderDriver;
private final EventRecorder eventRecorder;
private final AtomicBoolean started = new AtomicBoolean(false);
private final AtomicLong executorDelayCounter = new AtomicLong();
private final LongConsumer excessiveExecutionHandler;
private Strongback(Configurator config, Strongback previousInstance) {
boolean start = false;
if (previousInstance != null) {
start = previousInstance.started.get();
// Terminates all currently-scheduled commands and stops the executor's thread (if running) ...
previousInstance.doShutdown();
executables = previousInstance.executables;
switchReactor = previousInstance.switchReactor;
executables.unregister(previousInstance.dataRecorderDriver);
executables.unregister(previousInstance.eventRecorder);
executables.unregister(previousInstance.scheduler);
dataRecorderChannels = previousInstance.dataRecorderChannels;
excessiveExecutionHandler = previousInstance.excessiveExecutionHandler;
} else {
executables = new Executables();
switchReactor = new AsyncSwitchReactor();
executables.register(switchReactor);
dataRecorderChannels = new DataRecorderChannels();
excessiveExecutionHandler = config.excessiveExecutorDelayHandler;
}
loggers = config.loggersSupplier.get();
clock = config.timeSystemSupplier.get();
switch (config.executionWaitMode) {
case PARK:
metronome = Metronome.parker(config.executionPeriodInNanos, TimeUnit.NANOSECONDS, clock);
break;
case SLEEP:
metronome = Metronome.sleeper(config.executionPeriodInNanos, TimeUnit.NANOSECONDS, clock);
break;
case BUSY:
default:
metronome = Metronome.busy(config.executionPeriodInNanos, TimeUnit.NANOSECONDS, clock);
break;
}
// Create a new executor driver ...
executorDriver = new ExecutorDriver("Strongback Executor", executables, clock, metronome, loggers.apply("executor"),
monitorDelay(config.executionPeriodInNanos, TimeUnit.NANOSECONDS));
// Create a new event recorder ...
if (config.eventWriterFactory != null) {
eventRecorder = new AsyncEventRecorder(config.eventWriterFactory.get(), clock);
executables.register(eventRecorder);
} else {
eventRecorder = EventRecorder.noOp();
}
// Create a new scheduler that optionally records command state transitions. Note that we ignore everything in
// the previous instance's scheduler, since all commands would have been terminated (as intended) ...
CommandListener commandListener = config.recordCommandStateChanges ? this::recordCommand : this::recordNoCommands;
scheduler = new Scheduler(loggers.apply("scheduler"), commandListener);
executables.register(scheduler);
// Create a new data recorder driver ...
dataRecorderDriver = new DataRecorderDriver(dataRecorderChannels, config.dataWriterFactory);
executables.register(dataRecorderDriver);
// Start this if the previous was already started ...
if (previousInstance != null && start) {
doStart();
}
}
private LongConsumer monitorDelay(long executionInterval, TimeUnit unit) {
long intervalInMs = unit.toMillis(executionInterval);
return delayInMs -> {
if (delayInMs > intervalInMs) {
executorDelayCounter.incrementAndGet();
if (excessiveExecutionHandler != null) {
try {
excessiveExecutionHandler.accept(delayInMs);
} catch (Throwable t) {
logger().error(t, "Error with custom handler for excessive execution times");
}
} else {
logger().error("Unable to execute all activities within " + intervalInMs + " milliseconds!");
}
}
};
}
private void recordCommand(Command command, CommandState state) {
eventRecorder.record(command.getClass().getName(), state.ordinal());
}
private void recordNoCommands(Command command, CommandState state) {
}
private void doStart() {
if (!started.get()) {
try {
dataRecorderDriver.start();
} finally {
try {
executorDriver.start();
} finally {
started.set(true);
}
}
}
}
private void doRestart() {
if (started.get()) {
// Kill any remaining commands ...
scheduler.killAll();
} else {
try {
dataRecorderDriver.start();
} finally {
try {
executorDriver.start();
} finally {
started.set(true);
}
}
}
}
private void killCommandsAndFlush() {
if (started.get()) {
try {
// Kill any remaining commands ...
scheduler.killAll();
} finally {
// Finally flush the data recorder ...
dataRecorderDriver.flush();
}
}
}
private void doShutdown() {
try {
// First stop executing immediately; at this point, no executables will run ...
executorDriver.stop();
} finally {
try {
// Kill any remaining commands ...
scheduler.killAll();
} finally {
try {
// Finally flush the data recorder ...
dataRecorderDriver.stop();
} finally {
started.set(false);
}
}
}
}
}