/**
* 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;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.roda.core.RodaCoreFactory;
import org.roda.core.common.RodaUtils;
import org.roda.core.common.iterables.CloseableIterable;
import org.roda.core.data.common.RodaConstants;
import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.InvalidParameterException;
import org.roda.core.data.exceptions.JobAlreadyStartedException;
import org.roda.core.data.exceptions.JobException;
import org.roda.core.data.exceptions.JobInErrorException;
import org.roda.core.data.exceptions.JobIsStoppingException;
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.LiteOptionalWithCause;
import org.roda.core.data.v2.LiteRODAObject;
import org.roda.core.data.v2.common.OptionalWithCause;
import org.roda.core.data.v2.index.IsIndexed;
import org.roda.core.data.v2.index.filter.Filter;
import org.roda.core.data.v2.index.filter.OneOfManyFilterParameter;
import org.roda.core.data.v2.index.sort.SortParameter;
import org.roda.core.data.v2.index.sort.Sorter;
import org.roda.core.data.v2.jobs.Job;
import org.roda.core.data.v2.jobs.Job.JOB_STATE;
import org.roda.core.data.v2.jobs.PluginType;
import org.roda.core.index.IndexService;
import org.roda.core.index.utils.IterableIndexResult;
import org.roda.core.index.utils.SolrUtils;
import org.roda.core.model.LiteRODAObjectFactory;
import org.roda.core.model.ModelService;
import org.roda.core.model.utils.ModelUtils;
import org.roda.core.plugins.Plugin;
import org.roda.core.plugins.PluginException;
import org.roda.core.plugins.PluginOrchestrator;
import org.roda.core.plugins.orchestrate.akka.AkkaJobsManager;
import org.roda.core.plugins.orchestrate.akka.DeadLetterActor;
import org.roda.core.plugins.orchestrate.akka.Messages;
import org.roda.core.plugins.orchestrate.akka.Messages.JobPartialUpdate;
import org.roda.core.plugins.orchestrate.akka.Messages.JobStateUpdated;
import org.roda.core.plugins.plugins.PluginHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.AllDeadLetters;
import akka.actor.Props;
import akka.actor.Terminated;
import akka.dispatch.OnComplete;
import akka.pattern.Patterns;
import akka.util.Timeout;
import scala.concurrent.Await;
import scala.concurrent.Future;
import scala.concurrent.duration.Duration;
/*
* 20160520 hsilva: use kamon to obtain metrics about akka, Graphite & Grafana for collecting and dashboard (http://kamon.io/integrations/akka/overview/ &
* http://www.lightbend.com/activator/template/akka-monitoring-kamon-statsd)
*
* */
public class AkkaEmbeddedPluginOrchestrator implements PluginOrchestrator {
private static final Logger LOGGER = LoggerFactory.getLogger(AkkaEmbeddedPluginOrchestrator.class);
private final IndexService index;
private final ModelService model;
private ActorSystem jobsSystem;
private ActorRef jobsManager;
private int maxNumberOfJobsInParallel;
// Map<jobId, ActorRef>
private Map<String, ActorRef> runningJobs;
// List<jobId>
private List<String> stoppingJobs;
// List<jobId>
private List<String> inErrorJobs;
public AkkaEmbeddedPluginOrchestrator() {
maxNumberOfJobsInParallel = JobsHelper.getMaxNumberOfJobsInParallel();
index = RodaCoreFactory.getIndexService();
model = RodaCoreFactory.getModelService();
runningJobs = new HashMap<>();
stoppingJobs = new ArrayList<>();
inErrorJobs = new ArrayList<>();
Config akkaConfig = getAkkaConfiguration();
jobsSystem = ActorSystem.create("JobsSystem", akkaConfig);
// 20170105 hsilva: subscribe all dead letter so they are logged
jobsSystem.eventStream().subscribe(jobsSystem.actorOf(Props.create(DeadLetterActor.class)), AllDeadLetters.class);
jobsManager = jobsSystem.actorOf(Props.create(AkkaJobsManager.class, maxNumberOfJobsInParallel), "jobsManager");
}
private Config getAkkaConfiguration() {
InputStream originStream = RodaCoreFactory
.getConfigurationFileAsStream(RodaConstants.CORE_ORCHESTRATOR_FOLDER + "/application.conf");
Config akkaConfig = null;
try {
String configAsString = IOUtils.toString(originStream, RodaConstants.DEFAULT_ENCODING);
akkaConfig = ConfigFactory.parseString(configAsString);
} catch (IOException e) {
LOGGER.error("Could not load Akka configuration", e);
} finally {
RodaUtils.closeQuietly(originStream);
}
return akkaConfig;
}
@Override
public void setup() {
// do nothing
}
@Override
public void shutdown() {
LOGGER.info("Going to shutdown actor system (which will be done asynchronously)");
Future<Terminated> terminate = jobsSystem.terminate();
terminate.onComplete(new OnComplete<Terminated>() {
@Override
public void onComplete(Throwable failure, Terminated result) {
if (failure != null) {
LOGGER.error("Error while shutting down actor system", failure);
} else {
LOGGER.info("Done shutting down actor system");
}
}
}, jobsSystem.dispatcher());
}
@Override
public <T extends IsRODAObject, T1 extends IsIndexed> void runPluginFromIndex(Object context, Class<T1> classToActOn,
Filter filter, Plugin<T> plugin) {
try {
LOGGER.info("Starting {} (which will be done asynchronously)", plugin.getName());
ActorRef jobActor = (ActorRef) context;
ActorRef jobStateInfoActor = getJobContextInformation(PluginHelper.getJobId(plugin));
int blockSize = JobsHelper.getBlockSize();
Plugin<T> innerPlugin;
Class<T> modelClassToActOn = (Class<T>) ModelUtils.giveRespectiveModelClass(classToActOn);
jobStateInfoActor.tell(new Messages.PluginBeforeAllExecuteIsReady<>(plugin), jobActor);
List<String> liteFields = SolrUtils.getClassLiteFields(classToActOn);
Iterator<T1> findAllIterator = index
.findAll(classToActOn, filter, new Sorter(new SortParameter(RodaConstants.INDEX_UUID, true)), liteFields)
.iterator();
List<T1> indexObjects = new ArrayList<>();
while (findAllIterator.hasNext()) {
if (indexObjects.size() == blockSize) {
innerPlugin = getNewPluginInstanceAndInitJobPluginInfo(plugin, modelClassToActOn, blockSize, jobActor);
jobStateInfoActor.tell(new Messages.PluginExecuteIsReady<>(innerPlugin,
LiteRODAObjectFactory.transformIntoLiteWithCause(model, indexObjects)), jobActor);
indexObjects = new ArrayList<>();
}
indexObjects.add(findAllIterator.next());
}
if (!indexObjects.isEmpty()) {
innerPlugin = getNewPluginInstanceAndInitJobPluginInfo(plugin, modelClassToActOn, indexObjects.size(),
jobActor);
jobStateInfoActor.tell(new Messages.PluginExecuteIsReady<>(innerPlugin,
LiteRODAObjectFactory.transformIntoLiteWithCause(model, indexObjects)), jobActor);
}
jobStateInfoActor.tell(new Messages.JobInitEnded(), jobActor);
} catch (JobIsStoppingException | JobInErrorException e) {
// do nothing
} catch (Exception e) {
LOGGER.error("Error running plugin from index", e);
JobsHelper.updateJobState(plugin, JOB_STATE.FAILED_TO_COMPLETE, e);
}
}
@Override
public <T extends IsRODAObject> void runPluginOnObjects(Object context, Plugin<T> plugin, Class<T> objectClass,
List<String> uuids) {
try {
LOGGER.info("Starting {} (which will be done asynchronously)", plugin.getName());
ActorRef jobActor = (ActorRef) context;
ActorRef jobStateInfoActor = getJobContextInformation(PluginHelper.getJobId(plugin));
int blockSize = JobsHelper.getBlockSize();
List<T> objects = JobsHelper.getObjectsFromUUID(model, index, objectClass, uuids);
Iterator<T> iter = objects.iterator();
Plugin<T> innerPlugin;
jobStateInfoActor.tell(new Messages.PluginBeforeAllExecuteIsReady<>(plugin), jobActor);
List<T> block = new ArrayList<>();
while (iter.hasNext()) {
if (block.size() == blockSize) {
innerPlugin = getNewPluginInstanceAndInitJobPluginInfo(plugin, objectClass, blockSize, jobActor);
jobStateInfoActor.tell(new Messages.PluginExecuteIsReady<>(innerPlugin,
LiteRODAObjectFactory.transformIntoLiteWithCause(model, block)), jobActor);
block = new ArrayList<>();
}
block.add(iter.next());
}
if (!block.isEmpty()) {
innerPlugin = getNewPluginInstanceAndInitJobPluginInfo(plugin, objectClass, block.size(), jobActor);
jobStateInfoActor.tell(new Messages.PluginExecuteIsReady<>(innerPlugin,
LiteRODAObjectFactory.transformIntoLiteWithCause(model, block)), jobActor);
}
jobStateInfoActor.tell(new Messages.JobInitEnded(), jobActor);
} catch (JobIsStoppingException | JobInErrorException e) {
// do nothing
} catch (Exception e) {
LOGGER.error("Error running plugin on RODA Objects ({})", objectClass.getSimpleName(), e);
JobsHelper.updateJobState(plugin, JOB_STATE.FAILED_TO_COMPLETE, e);
}
}
@Override
public <T extends IsRODAObject> void runPluginOnAllObjects(Object context, Plugin<T> plugin, Class<T> objectClass) {
try {
LOGGER.info("Starting {} (which will be done asynchronously)", plugin.getName());
ActorRef jobActor = (ActorRef) context;
ActorRef jobStateInfoActor = getJobContextInformation(PluginHelper.getJobId(plugin));
int blockSize = JobsHelper.getBlockSize();
CloseableIterable<OptionalWithCause<LiteRODAObject>> objects = model.listLite(objectClass);
Iterator<OptionalWithCause<LiteRODAObject>> iter = objects.iterator();
Plugin<T> innerPlugin;
jobStateInfoActor.tell(new Messages.PluginBeforeAllExecuteIsReady<>(plugin), jobActor);
List<LiteOptionalWithCause> block = new ArrayList<>();
while (iter.hasNext()) {
if (block.size() == blockSize) {
innerPlugin = getNewPluginInstanceAndInitJobPluginInfo(plugin, objectClass, blockSize, jobActor);
jobStateInfoActor.tell(new Messages.PluginExecuteIsReady<>(innerPlugin, block), jobActor);
block = new ArrayList<>();
}
OptionalWithCause<LiteRODAObject> nextObject = iter.next();
if (nextObject.isPresent()) {
block.add(LiteOptionalWithCause.of(nextObject.get()));
} else {
LOGGER.error("Cannot process object", nextObject.getCause());
}
}
if (!block.isEmpty()) {
innerPlugin = getNewPluginInstanceAndInitJobPluginInfo(plugin, objectClass, block.size(), jobActor);
jobStateInfoActor.tell(new Messages.PluginExecuteIsReady<>(innerPlugin, block), jobActor);
}
jobStateInfoActor.tell(new Messages.JobInitEnded(), jobActor);
IOUtils.closeQuietly(objects);
} catch (JobIsStoppingException | JobInErrorException e) {
// do nothing
} catch (Exception e) {
LOGGER.error("Error running plugin on all objects", e);
JobsHelper.updateJobState(plugin, JOB_STATE.FAILED_TO_COMPLETE, e);
}
}
@Override
public <T extends IsRODAObject> void runPlugin(Object context, Plugin<T> plugin) {
try {
LOGGER.info("Starting {} (which will be done asynchronously)", plugin.getName());
ActorRef jobActor = (ActorRef) context;
ActorRef jobStateInfoActor = getJobContextInformation(PluginHelper.getJobId(plugin));
initJobPluginInfo(plugin, 0, jobActor);
jobStateInfoActor.tell(new Messages.PluginBeforeAllExecuteIsReady<>(plugin), jobActor);
jobStateInfoActor.tell(new Messages.PluginExecuteIsReady<>(plugin, Collections.emptyList()), jobActor);
jobStateInfoActor.tell(new Messages.JobInitEnded(), jobActor);
} catch (JobIsStoppingException | JobInErrorException e) {
// do nothing
} catch (Exception e) {
LOGGER.error("Error running plugin", e);
JobsHelper.updateJobState(plugin, JOB_STATE.FAILED_TO_COMPLETE, e);
}
}
private <T extends IsRODAObject> void initJobPluginInfo(Plugin<T> plugin, int objectsCount, ActorRef jobActor)
throws InvalidParameterException, PluginException, JobIsStoppingException, JobInErrorException {
// keep track of each job/plugin relation
String jobId = PluginHelper.getJobId(plugin);
if (jobId != null && runningJobs.get(jobId) != null) {
// see if job is stopping
if (stoppingJobs.contains(jobId)) {
throw new JobIsStoppingException();
}
// see if job is in error
if (inErrorJobs.contains(jobId)) {
throw new JobInErrorException();
}
ActorRef jobStateInfoActor = runningJobs.get(jobId);
if (PluginType.INGEST == plugin.getType()) {
IngestJobPluginInfo jobPluginInfo = new IngestJobPluginInfo();
initJobPluginInfo(plugin, jobActor, jobStateInfoActor, jobPluginInfo, objectsCount);
plugin.injectJobPluginInfo(jobPluginInfo);
} else if (PluginType.MISC == plugin.getType() || PluginType.AIP_TO_AIP == plugin.getType()) {
SimpleJobPluginInfo jobPluginInfo = new SimpleJobPluginInfo();
initJobPluginInfo(plugin, jobActor, jobStateInfoActor, jobPluginInfo, objectsCount);
plugin.injectJobPluginInfo(jobPluginInfo);
}
} else {
LOGGER.error("Error while trying to init plugin. Cause: unable to find out job id");
}
}
private <T extends IsRODAObject> Plugin<T> getNewPluginInstanceAndInitJobPluginInfo(Plugin<T> plugin,
Class<T> pluginClass, int objectsCount, ActorRef jobActor)
throws InvalidParameterException, PluginException, JobIsStoppingException, JobInErrorException {
Plugin<T> innerPlugin = RodaCoreFactory.getPluginManager().getPlugin(plugin.getClass().getName(), pluginClass);
innerPlugin.setParameterValues(plugin.getParameterValues());
// keep track of each job/plugin relation
String jobId = PluginHelper.getJobId(innerPlugin);
if (jobId != null && runningJobs.get(jobId) != null) {
// see if job is stopping
if (stoppingJobs.contains(jobId)) {
throw new JobIsStoppingException();
}
// see if job is in error
if (inErrorJobs.contains(jobId)) {
throw new JobInErrorException();
}
ActorRef jobStateInfoActor = getJobContextInformation(PluginHelper.getJobId(plugin));
if (PluginType.INGEST == plugin.getType()) {
IngestJobPluginInfo jobPluginInfo = new IngestJobPluginInfo();
initJobPluginInfo(innerPlugin, jobActor, jobStateInfoActor, jobPluginInfo, objectsCount);
innerPlugin.injectJobPluginInfo(jobPluginInfo);
} else {
SimpleJobPluginInfo jobPluginInfo = new SimpleJobPluginInfo();
initJobPluginInfo(innerPlugin, jobActor, jobStateInfoActor, jobPluginInfo, objectsCount);
innerPlugin.injectJobPluginInfo(jobPluginInfo);
}
} else {
LOGGER.error("Error while trying to init plugin. Cause: unable to find out job id");
}
return innerPlugin;
}
private <T extends IsRODAObject> void initJobPluginInfo(Plugin<T> innerPlugin, ActorRef jobActor,
ActorRef jobStateInfoActor, JobPluginInfo jobPluginInfo, int objectsCount) {
jobPluginInfo.setSourceObjectsCount(objectsCount);
jobPluginInfo.setSourceObjectsWaitingToBeProcessed(objectsCount);
jobStateInfoActor.tell(new Messages.JobInfoUpdated(innerPlugin, jobPluginInfo), jobActor);
}
@Override
public void executeJob(Job job, boolean async) throws JobAlreadyStartedException {
LOGGER.info("Adding job '{}' ({}) to be executed", job.getName(), job.getId());
if (runningJobs.containsKey(job.getId())) {
LOGGER.info("Job '{}' ({}) is already queued to be executed", job.getName(), job.getId());
throw new JobAlreadyStartedException();
} else {
if (async) {
jobsManager.tell(job, ActorRef.noSender());
} else {
int timeoutInSeconds = JobsHelper.getSyncTimeout();
Timeout timeout = new Timeout(Duration.create(timeoutInSeconds, "seconds"));
Future<Object> future = Patterns.ask(jobsManager, job, timeout);
try {
Await.result(future, timeout.duration());
} catch (Exception e) {
LOGGER.error("Error executing job synchronously", e);
}
}
LOGGER.info("Success adding job '{}' ({}) to be executed", job.getName(), job.getId());
}
}
@Override
public void stopJob(Job job) {
String jobId = job.getId();
if (jobId != null && runningJobs.get(jobId) != null) {
stoppingJobs.add(jobId);
ActorRef jobStateInfoActor = runningJobs.get(jobId);
jobStateInfoActor.tell(new Messages.JobStop(), ActorRef.noSender());
}
}
@Override
public void cleanUnfinishedJobs() {
cleanUnfinishedJobs(findUnfinishedJobs());
}
private IterableIndexResult<Job> findUnfinishedJobs() {
Filter filter = new Filter(new OneOfManyFilterParameter(RodaConstants.JOB_STATE, Job.nonFinalStateList()));
return index.findAll(Job.class, filter, new ArrayList<>());
}
private void cleanUnfinishedJobs(IterableIndexResult<Job> unfinishedJobs) {
List<Job> unfinishedJobsList = new ArrayList<>();
unfinishedJobs.forEach(job -> unfinishedJobsList.add(job));
List<String> jobsToBeDeletedFromIndex = new ArrayList<>();
for (Job job : unfinishedJobsList) {
try {
Job jobToBeCleaned = model.retrieveJob(job.getId());
// cleanup job related objects (aips, sips, etc.)
JobsHelper.doJobObjectsCleanup(job, model, index);
// only after deleting all the objects, delete the job
JobsHelper.updateJobInTheStateStartedOrCreated(jobToBeCleaned);
model.createOrUpdateJob(jobToBeCleaned);
} catch (NotFoundException e) {
jobsToBeDeletedFromIndex.add(job.getId());
} catch (RequestNotValidException | GenericException | AuthorizationDeniedException e) {
LOGGER.error("Unable to get/update Job", e);
}
}
if (!jobsToBeDeletedFromIndex.isEmpty()) {
index.deleteSilently(Job.class, jobsToBeDeletedFromIndex);
}
}
@Override
public <T extends IsRODAObject> void updateJob(Plugin<T> plugin, JobPartialUpdate partialUpdate) {
String jobId = PluginHelper.getJobId(plugin);
if (jobId != null && runningJobs.get(jobId) != null) {
ActorRef jobStateInfoActor = runningJobs.get(jobId);
jobStateInfoActor.tell(partialUpdate, ActorRef.noSender());
if (partialUpdate instanceof JobStateUpdated && Job.isFinalState(((JobStateUpdated) partialUpdate).getState())) {
runningJobs.remove(jobId);
stoppingJobs.remove(jobId);
inErrorJobs.remove(jobId);
}
} else {
LOGGER.error("Got job id or job information null when updating Job state");
}
}
@Override
public void setJobContextInformation(String jobId, Object object) {
runningJobs.put(jobId, (ActorRef) object);
}
public ActorRef getJobContextInformation(String jobId) {
return runningJobs.get(jobId);
}
@Override
public <T extends IsRODAObject> void updateJobInformation(Plugin<T> plugin, JobPluginInfo info) throws JobException {
String jobId = PluginHelper.getJobId(plugin);
if (jobId != null && runningJobs.get(jobId) != null) {
ActorRef jobStateInfoActor = runningJobs.get(jobId);
jobStateInfoActor.tell(new Messages.JobInfoUpdated(plugin, info), ActorRef.noSender());
} else {
throw new JobException("Job id or job information is null");
}
}
@Override
public void setJobInError(String jobId) {
inErrorJobs.add(jobId);
}
}