/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE file at the root of the source
* tree and available online at
*
* https://github.com/keeps/roda
*/
package org.roda.core.plugins.orchestrate.akka;
import java.util.ArrayList;
import java.util.Optional;
import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.NotFoundException;
import org.roda.core.data.exceptions.RequestNotValidException;
import org.roda.core.data.v2.IsRODAObject;
import org.roda.core.data.v2.index.select.SelectedItems;
import org.roda.core.data.v2.index.select.SelectedItemsList;
import org.roda.core.data.v2.jobs.Job;
import org.roda.core.data.v2.jobs.Job.JOB_STATE;
import org.roda.core.index.IndexService;
import org.roda.core.plugins.Plugin;
import org.roda.core.plugins.PluginException;
import org.roda.core.plugins.orchestrate.JobInfo;
import org.roda.core.plugins.orchestrate.JobPluginInfo;
import org.roda.core.plugins.orchestrate.JobsHelper;
import org.roda.core.plugins.plugins.PluginHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.actor.Terminated;
import akka.routing.RoundRobinPool;
public class AkkaJobStateInfoActor extends AkkaBaseActor {
private static final Logger LOGGER = LoggerFactory.getLogger(AkkaJobStateInfoActor.class);
private JobInfo jobInfo;
private Plugin<?> plugin;
private ActorRef jobCreator;
private ActorRef jobsManager;
private ActorRef workersRouter;
boolean stopping = false;
boolean errorDuringBeforeAll = false;
private String jobId;
// metrics
// private Map<String, Histogram> stateMessagesMetrics;
private Histogram stateMessagesMetricsHistogram;
public AkkaJobStateInfoActor(Plugin<?> plugin, ActorRef jobCreator, ActorRef jobsManager, String jobId,
int numberOfJobsWorkers) {
super();
jobInfo = new JobInfo();
this.plugin = plugin;
this.jobCreator = jobCreator;
this.jobsManager = jobsManager;
this.jobId = jobId;
LOGGER.debug("Starting AkkaJobStateInfoActor router with {} actors", numberOfJobsWorkers);
Props workersProps = new RoundRobinPool(numberOfJobsWorkers).props(Props.create(AkkaWorkerActor.class));
workersRouter = getContext().actorOf(workersProps, "WorkersRouter");
// 20160914 hsilva: watch child events, so when they stop we can react
getContext().watch(workersRouter);
JobsHelper.createJobWorkingDirectory(jobId);
String className = AkkaJobStateInfoActor.class.getSimpleName();
// stateMessagesMetrics = new HashMap<>();
stateMessagesMetricsHistogram = getMetricRegistry()
.histogram(MetricRegistry.name(className, "msgCreationToProcessingStartedInMilis"));
}
@Override
public void onReceive(Object msg) throws Exception {
super.setup(msg);
if (msg instanceof Messages.JobStateUpdated) {
handleJobStateUpdated(msg);
} else if (msg instanceof Messages.JobSourceObjectsUpdated) {
handleJobSourceObjectsUpdated(msg);
} else if (msg instanceof Messages.JobInfoUpdated) {
handleJobInfoUpdated(msg);
} else if (msg instanceof Messages.JobStop) {
handleJobStop(msg);
} else if (msg instanceof Terminated) {
handleTerminated(msg);
} else if (msg instanceof Messages.PluginExecuteIsReady) {
handleExecuteIsReady(msg);
} else if (msg instanceof Messages.JobInitEnded) {
handleJobInitEnded(msg);
} else if (msg instanceof Messages.PluginBeforeAllExecuteIsReady) {
handleBeforeAllExecuteIsReady(msg);
} else if (msg instanceof Messages.PluginExecuteIsDone) {
handleExecuteIsDone(msg);
} else if (msg instanceof Messages.PluginAfterAllExecuteIsDone) {
handleAfterAllExecuteIsDone(msg);
} else if (msg instanceof Messages.JobCleanup) {
handleJobCleanup(msg);
} else {
LOGGER.error("Received a message that don't know how to process ({})...", msg.getClass().getName());
unhandled(msg);
}
}
private void handleJobStateUpdated(Object msg) {
Messages.JobStateUpdated message = (Messages.JobStateUpdated) msg;
markMessageProcessingAsStarted(message);
Plugin<?> p = message.getPlugin() == null ? this.plugin : message.getPlugin();
try {
Job job = PluginHelper.getJob(p, getIndex());
LOGGER.info("Setting job '{}' ({}) state to {}. Details: {}", job.getName(), job.getId(), message.getState(),
message.getStateDatails().orElse("NO DETAILS"));
} catch (NotFoundException | GenericException e) {
LOGGER.warn("Unable to get Job from index to log its state change. Reason: {}", e.getMessage());
}
JobsHelper.updateJobState(p, getModel(), message.getState(), message.getStateDatails());
if (Job.isFinalState(message.getState())) {
// 20160817 hsilva: the following instruction is needed for the "sync"
// execution of a job (i.e. for testing purposes)
jobCreator.tell("Done", getSelf());
jobsManager.tell(new Messages.JobsManagerJobEnded(jobId), getSelf());
JobsHelper.deleteJobWorkingDirectory(jobId);
getContext().stop(getSelf());
}
markMessageProcessingAsEnded(message);
}
private <T extends IsRODAObject> void handleJobSourceObjectsUpdated(Object msg) {
Messages.JobSourceObjectsUpdated message = (Messages.JobSourceObjectsUpdated) msg;
markMessageProcessingAsStarted(message);
try {
Job job = PluginHelper.getJob(plugin, getModel());
SelectedItems<?> sourceObjects = job.getSourceObjects();
if (sourceObjects instanceof SelectedItemsList) {
SelectedItemsList<T> items = (SelectedItemsList<T>) sourceObjects;
ArrayList<String> newIds = new ArrayList<>();
for (String oldId : items.getIds()) {
if (message.getOldToNewIds().get(oldId) != null) {
newIds.add(message.getOldToNewIds().get(oldId));
} else {
newIds.add(oldId);
}
}
items.setIds(newIds);
getModel().createOrUpdateJob(job);
}
} catch (NotFoundException | GenericException | RequestNotValidException | AuthorizationDeniedException e) {
LOGGER.error("Error while retrieving Job for doing an update", e);
}
markMessageProcessingAsEnded(message);
}
private void handleJobInfoUpdated(Object msg) {
Messages.JobInfoUpdated message = (Messages.JobInfoUpdated) msg;
markMessageProcessingAsStarted(message);
jobInfo.put(message.getPlugin(), message.getJobPluginInfo());
JobPluginInfo infoUpdated = message.getJobPluginInfo().processJobPluginInformation(message.getPlugin(), jobInfo);
jobInfo.setObjectsCount(infoUpdated.getSourceObjectsCount());
JobsHelper.updateJobInformation(message.getPlugin(), getModel(), infoUpdated);
markMessageProcessingAsEnded(message);
}
private void handleJobStop(Object msg) {
Messages.JobStop message = (Messages.JobStop) msg;
markMessageProcessingAsStarted(message);
getSelf().tell(new Messages.JobStateUpdated(plugin, JOB_STATE.STOPPING), getSelf());
stopping = true;
getContext().getChildren().forEach(e -> getContext().stop(e));
markMessageProcessingAsEnded(message);
}
private void handleTerminated(Object msg) {
LOGGER.trace("{} Started processing message {}", "NO_UUID", Terminated.class.getSimpleName());
boolean allChildrenAreDead = true;
if (stopping) {
for (ActorRef child : getContext().getChildren()) {
allChildrenAreDead = false;
break;
}
if (allChildrenAreDead) {
getSelf().tell(new Messages.JobStateUpdated(plugin, JOB_STATE.STOPPED), getSelf());
}
}
LOGGER.trace("{} Ended processing message {} (stopping={} allChildrenAreDead={})", "NO_UUID",
Terminated.class.getSimpleName(), stopping, allChildrenAreDead);
}
private void handleExecuteIsReady(Object msg) {
if (!errorDuringBeforeAll) {
Messages.PluginExecuteIsReady message = (Messages.PluginExecuteIsReady) msg;
markMessageProcessingAsStarted(message);
jobInfo.setStarted(message.getPlugin());
// 20160819 hsilva: the following it's just for debugging purposes
message.setHasBeenForwarded();
workersRouter.tell(message, getSelf());
markMessageProcessingAsEnded(message);
}
}
private void handleJobInitEnded(Object msg) {
Messages.JobInitEnded message = (Messages.JobInitEnded) msg;
markMessageProcessingAsStarted(message);
jobInfo.setInitEnded(true);
// INFO 20160630 hsilva: the following test is needed because messages can
// be out of order and a plugin might already arrived to the end
if (jobInfo.isDone()) {
workersRouter.tell(new Messages.PluginAfterAllExecuteIsReady(plugin), getSelf());
}
markMessageProcessingAsEnded(message);
}
private void handleBeforeAllExecuteIsReady(Object msg) throws PluginException {
Messages.PluginBeforeAllExecuteIsReady message = (Messages.PluginBeforeAllExecuteIsReady) msg;
markMessageProcessingAsStarted(message);
try {
message.getPlugin().beforeAllExecute(getIndex(), getModel(), getStorage());
// do nothing because if all goes good, the next messages are of type
// PluginExecuteIsReady
} catch (Throwable e) {
// 20170120 hsilva: it is required to catch Throwable as there are some
// linking errors that only will happen during the execution (e.g.
// java.lang.NoSuchMethodError)
errorDuringBeforeAll = true;
getPluginOrchestrator().setJobInError(PluginHelper.getJobId(plugin));
getSelf().tell(
new Messages.JobStateUpdated(plugin, JOB_STATE.FAILED_TO_COMPLETE, Optional.ofNullable(e.getMessage())),
getSelf());
}
markMessageProcessingAsEnded(message);
}
private void handleExecuteIsDone(Object msg) {
Messages.PluginExecuteIsDone message = (Messages.PluginExecuteIsDone) msg;
markMessageProcessingAsStarted(message);
jobInfo.setDone(message.getPlugin());
if (jobInfo.isDone() && jobInfo.isInitEnded()) {
workersRouter.tell(new Messages.PluginAfterAllExecuteIsReady(plugin), getSelf());
}
markMessageProcessingAsEnded(message);
}
private void handleAfterAllExecuteIsDone(Object msg) {
Messages.PluginAfterAllExecuteIsDone message = (Messages.PluginAfterAllExecuteIsDone) msg;
markMessageProcessingAsStarted(message);
getSelf().tell(new Messages.JobCleanup(), getSelf());
getSelf().tell(
new Messages.JobStateUpdated(plugin, message.isWithError() ? JOB_STATE.FAILED_TO_COMPLETE : JOB_STATE.COMPLETED),
getSelf());
markMessageProcessingAsEnded(message);
}
private void handleJobCleanup(Object msg) {
Messages.JobCleanup message = (Messages.JobCleanup) msg;
markMessageProcessingAsStarted(message);
try {
LOGGER.info("Doing job cleanup");
IndexService indexService = getIndex();
Job job = PluginHelper.getJob(plugin, indexService);
JobsHelper.doJobObjectsCleanup(job, super.getModel(), indexService);
LOGGER.info("Ended doing job cleanup");
} catch (NotFoundException | GenericException e) {
LOGGER.error("Unable to get Job for doing cleanup", e);
}
markMessageProcessingAsEnded(message);
}
private void markMessageProcessingAsStarted(Messages.AbstractMessage message) {
message.logProcessingStarted();
stateMessagesMetricsHistogram.update(message.getTimeSinceCreation());
}
private void markMessageProcessingAsEnded(Messages.AbstractMessage message) {
message.logProcessingEnded();
}
}