/*
* 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.command;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.strongback.Logger;
import org.strongback.Strongback;
import org.strongback.command.Scheduler.CommandListener;
/**
* Manages all of the executable state information for a {@link Command}.
*/
final class CommandRunner {
static interface Context {
Logger logger();
CommandListener listener();
static Context with(CommandListener listener, Logger logger) {
return new Context() {
@Override
public CommandListener listener() {
return listener;
}
@Override
public Logger logger() {
return logger;
}
};
}
}
private static final CommandListener NO_OP_LISTENER = CommandListener.noOp();
private static final Context DEFAULT_CONTEXT = Context.with(NO_OP_LISTENER,
Strongback.logger(CommandRunner.class.getName()));
private boolean timed = false;
private long timeoutInMillis;
private long endTime;
private volatile boolean cancelled = false;
private final Command command;
private CommandRunner[] children = null;
private CommandRunner next;
private CommandState state = CommandState.UNINITIALIZED;
private final Context context;
CommandRunner(Command command) {
// Just a command and no next is a leaf
this(DEFAULT_CONTEXT, command);
}
CommandRunner(Context context, Command command) {
// Just a command and no next is a leaf
this(context, null, command);
}
CommandRunner(Context context, CommandRunner next, Command command) {
// A command and a next is a node
this.command = command;
this.next = next;
this.timeoutInMillis = (long) (command.getTimeoutInSeconds() * 1000);
this.context = context != null ? context : DEFAULT_CONTEXT;
}
CommandRunner(Context context, CommandRunner next, CommandRunner... commands) {
// A next and several children is a branch
if (commands.length != 0) this.children = commands;
this.next = next;
this.command = null;
this.context = context != null ? context : DEFAULT_CONTEXT;
}
/**
* Steps through all of the state logic for its {@link Command}.
*
* @param timeInMillis the current system time in milliseconds
* @return {@code true} if this {@link CommandRunner} is ready to be terminated; {@code false} otherwise
*/
boolean step(long timeInMillis) {
if (cancelled) {
state = CommandState.INTERUPTED;
}
// if we have a timeout
if (timeoutInMillis != 0) {
endTime = timeInMillis + timeoutInMillis;
timed = true;
timeoutInMillis = 0;
}
if (timed && timeInMillis >= endTime) {
state = CommandState.FINISHED;
}
// If we don't have children or a command, we are a fork and must be done
if (children == null && command == null) return true;
// If we have children, but no command, we are a branch
if (children != null && command == null) {
assert command == null;
// We are done as long as none of our children are not
boolean childrenDone = true;
for (CommandRunner command : children) {
if (!command.step(timeInMillis)) childrenDone = false;
}
return childrenDone;
}
// If we have a command, but no children, manage our command
// If we are uninitialized initialize us
if (state == CommandState.UNINITIALIZED) {
try {
listener().record(command, state);
command.initialize();
state = CommandState.RUNNING;
} catch (Throwable t) {
logger().error(t, "Error while initializing " + command.getClass().getName() + " command: " + command);
state = CommandState.INTERUPTED;
}
}
// If we should be running
if (state == CommandState.RUNNING) {
try {
listener().record(command, state);
if (command.execute()) state = CommandState.FINISHED;
} catch (Throwable t) {
logger().error(t, "Error while executing " + command.getClass().getName() + " command: " + command);
state = CommandState.INTERUPTED;
}
}
// If we were interrupted
if (state == CommandState.INTERUPTED) {
try {
listener().record(command, state);
command.interrupted();
} catch (Throwable t) {
logger().error(t, "Error while interrupting " + command.getClass().getName() + " command: " + command);
}
state = CommandState.FINALIZED;
}
// If we are pending finalization
if (state == CommandState.FINISHED) {
listener().record(command, state);
try {
command.end();
} catch (Throwable t) {
logger().error(t, "Error while ending " + command.getClass().getName() + " command: " + command);
}
state = CommandState.FINALIZED;
listener().record(command, state);
}
return state == CommandState.FINALIZED;
}
private Logger logger() {
return context.logger();
}
private CommandListener listener() {
return context.listener();
}
void after(Commands commandList) {
// Add our own next (if we have one) and the next of our children (if we have them)
if (next != null) {
commandList.add(next);
}
if (children != null) {
for (CommandRunner command : children)
command.after(commandList);
}
}
/**
* Schedules its {@link Command} to be canceled next iteration.
*/
public void cancel() {
// We want to change the state to INTERRUPTED, but for thread-safety we can't actually change the `state` field here
// since it is actively used within the `step(...)` method. So instead, we'll set the `cancelled` flag (this is the
// only place this is done) ...
cancelled = true;
if (children != null) {
for (CommandRunner runner : children) {
runner.cancel();
}
}
if (next != null) next = null;
}
@Override
public String toString() {
if (command != null) {
return next == null ? command.toString() : command.toString() + " -> " + next;
}
if (children != null) {
return next == null ? Arrays.toString(children) : Arrays.toString(children) + " -> " + next;
}
return "FORK<" + next.toString() + ">";
}
CommandState state() {
return state;
}
boolean isCancelled() {
return cancelled;
}
public boolean isInterruptible() {
if (command != null) {
return command.isInterruptible();
} else if (children != null) {
for (CommandRunner runner : children) {
if (!runner.isInterruptible()) return false;
}
}
return true;
}
public Set<Requirable> getRequired() {
Set<Requirable> required = new HashSet<>();
if (command != null) {
required.addAll(command.getRequirements());
} else if (children != null) {
for (CommandRunner runner : children) {
required.addAll(runner.getRequired());
}
}
return required;
}
}