/** * 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.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.concurrent.TimeUnit; import org.roda.core.data.v2.jobs.Job; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.Counter; import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import akka.actor.ActorRef; import akka.actor.Props; import akka.routing.RoundRobinPool; import scala.concurrent.duration.Duration; public class AkkaJobsManager extends AkkaBaseActor { private static final Logger LOGGER = LoggerFactory.getLogger(AkkaJobsManager.class); // state private int maxNumberOfJobsInParallel; private Queue<JobWaiting> jobsWaiting; private Map<String, ActorRef> jobsWaitingCreators; private ActorRef jobsRouter; // metrics private Counter jobsBeingExecuted; private Counter jobsWaitingToBeExecuted; private Counter ticksWaitingToBeProcessed; private Histogram jobsBeingExecutedHisto; private Histogram jobsWaitingToBeExecutedHisto; private Histogram jobsTimeInTheQueueInMilis; public AkkaJobsManager(int maxNumberOfJobsInParallel) { super(); this.maxNumberOfJobsInParallel = maxNumberOfJobsInParallel; this.jobsWaiting = new LinkedList<>(); this.jobsWaitingCreators = new HashMap<>(); Props jobsProps = new RoundRobinPool(maxNumberOfJobsInParallel).props(Props.create(AkkaJobActor.class, getSelf())); jobsRouter = getContext().actorOf(jobsProps, "JobsRouter"); initMetrics(maxNumberOfJobsInParallel); getContext().system().scheduler().schedule(Duration.create(0, TimeUnit.MILLISECONDS), Duration.create(2, TimeUnit.SECONDS), () -> { if (jobsWaitingToBeExecuted.getCount() > 0) { sendTick(); } }, getContext().system().dispatcher()); } @Override public void onReceive(Object msg) throws Throwable { if (msg instanceof Job) { Job job = (Job) msg; if (jobsWaitingToBeExecuted.getCount() == 0 && jobsBeingExecuted.getCount() < maxNumberOfJobsInParallel) { sendJobForExecution(msg, job); } else { queueJob(job, getSender()); } } else if (msg instanceof Messages.JobsManagerTick) { if (jobsWaitingToBeExecuted.getCount() > 0 && jobsBeingExecuted.getCount() < maxNumberOfJobsInParallel) { while (jobsWaitingToBeExecuted.getCount() > 0 && jobsBeingExecuted.getCount() < maxNumberOfJobsInParallel) { dequeueJob(); } } ticksWaitingToBeProcessed.dec(); } else if (msg instanceof Messages.JobsManagerJobEnded) { updateJobsBeingExecuted(false); sendTick(); log("The end for job", ((Messages.JobsManagerJobEnded) msg).getJobId()); } else { LOGGER.error("Received a message that don't know how to process ({})...", msg.getClass().getName()); unhandled(msg); } } private void sendTick() { if (ticksWaitingToBeProcessed.getCount() == 0) { self().tell(new Messages.JobsManagerTick(), self()); ticksWaitingToBeProcessed.inc(); } } private void sendJobForExecution(Object msg, Job job) { updateJobsBeingExecuted(true); jobsTimeInTheQueueInMilis.update(0); jobsRouter.tell(msg, getSender()); log("Will execute job", job.getId()); } private void updateJobsBeingExecuted(boolean increment) { if (increment) { jobsBeingExecuted.inc(); } else { jobsBeingExecuted.dec(); } jobsBeingExecutedHisto.update(jobsBeingExecuted.getCount()); } private void updateJobsWaitingToBeExecuted(boolean increment) { if (increment) { jobsWaitingToBeExecuted.inc(); } else { jobsWaitingToBeExecuted.dec(); } jobsWaitingToBeExecutedHisto.update(jobsWaitingToBeExecuted.getCount()); } private void queueJob(Job job, ActorRef sender) { jobsWaiting.offer(new JobWaiting(job)); jobsWaitingCreators.put(job.getId(), sender); updateJobsWaitingToBeExecuted(true); log("Queued job", job.getId()); } private Job dequeueJob() { JobWaiting jobWaiting = jobsWaiting.remove(); jobsTimeInTheQueueInMilis.update(jobWaiting.timeInQueue()); Job job = jobWaiting.job; ActorRef jobCreator = jobsWaitingCreators.remove(job.getId()); jobsRouter.tell(job, jobCreator); updateJobsBeingExecuted(true); updateJobsWaitingToBeExecuted(false); log("Dequeued job", job.getId()); return job; } private void log(String msg, String jobId) { LOGGER.info("{} '{}' (max: {}| exec: {}| wait: {})", msg, jobId, maxNumberOfJobsInParallel, jobsBeingExecuted.getCount(), jobsWaitingToBeExecuted.getCount()); } private void initMetrics(int maxNumberOfJobsInParallel) { MetricRegistry metrics = getMetricRegistry(); String className = AkkaJobsManager.class.getSimpleName(); Counter maxNumberOfJobsInParallelCounter = metrics .counter(MetricRegistry.name(className, "maxNumberOfJobsInParallel")); maxNumberOfJobsInParallelCounter.inc(maxNumberOfJobsInParallel); jobsBeingExecuted = metrics.counter(MetricRegistry.name(className, "jobsBeingExecuted")); jobsWaitingToBeExecuted = metrics.counter(MetricRegistry.name(className, "jobsWaitingToBeExecuted")); ticksWaitingToBeProcessed = metrics.counter(MetricRegistry.name(className, "ticksWaitingToBeProcessed")); jobsBeingExecutedHisto = metrics.histogram(MetricRegistry.name(className, "jobsBeingExecutedHistogram")); jobsWaitingToBeExecutedHisto = metrics .histogram(MetricRegistry.name(className, "jobsWaitingToBeExecutedHistogram")); jobsTimeInTheQueueInMilis = metrics.histogram(MetricRegistry.name(className, "jobsTimeInTheQueueInMilis")); } private class JobWaiting { public Job job; private long queuedIn; public JobWaiting(Job job) { this.job = job; this.queuedIn = new Date().getTime(); } /** Time in miliseconds */ public long timeInQueue() { return new Date().getTime() - queuedIn; } } }