All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scavenger.backend.master.LoadBalancer.scala Maven / Gradle / Ivy

The newest version!
package scavenger.backend.master

import akka.actor._
import scala.collection.mutable
import scala.collection.mutable.HashMap
import scala.concurrent.{Future, Promise, ExecutionContext}
import scavenger._
import scavenger.backend._
import scavenger.backend.worker.Worker.WorkerHere
import scavenger.categories.formalccc
import LastMessageTimeMonitoring.RemoteNodeNotResponding

/** This trait implements load balancing among multiple
  * worker nodes.
  *
  * @since 2.1
  * @author Andrey Tyukin
  */
trait LoadBalancer 
extends Actor 
with ActorLogging 
with SimpleComputationExecutor
with ContextProvider 
with Remindable
with LastMessageTimeMonitoring {

  import context.dispatcher
  
  /** Stores internal jobs that
    * have not yet been assigned to a worker
    */
  private val queue = mutable.Queue.empty[InternalJob]

  /** Assignment of worker-ActorRef's to the currently processed job.
    */
  private val assignedJobs: mutable.Map[ActorRef, Option[InternalJob]] = 
    HashMap.empty[ActorRef, Option[InternalJob]]

  /** Perform a simple computation that can be delegated.
    */
  def computeSimplified[X](r: Computation[X]): Future[X] = {
    // we simply create a promise in the promise-map, and enqueue the job
    val p = Promise[Any]
    val label = toInternalLabel(r.identifier)
    promises(label) = p
    enqueueSimple(label, r)
    val result = p.future.map{ a => a.asInstanceOf[X] }
    result
  }

  /** Appends an internal job id to a job and puts it into the job queue.
    */
  private def enqueueSimple(label: InternalLabel, job: Computation[Any]): Unit = 
  { 
    val internalJob = InternalJob(label, job)
    queue.enqueue(internalJob)
    // notify all workers that there is something to do
    for (worker <- idleWorkers) worker ! JobsAvailable
  }

  /** Assigns a job to worker.
    *
    * Just a way to make things a little safer (e.g. prevents you from
    * sending `Computation`s to workers).
    */
  private def sendJobToWorker(j: InternalJob, w: ActorRef): Unit = {
    log.debug("sendJobToWorker: job = {}",j)
    assert(j != null, "The job should be not null")
    w ! j
  }
  
  /** Makes sure that we know about the existence of the worker
    */
  protected[master] def register(worker: ActorRef): Unit = {
    context.watch(worker)
    if (!assignedJobs.contains(worker)) {
      assignedJobs(worker) = None
      log.info("Registered worker " + worker.path.name)
    }
  }

  /** Tries to assign a job to a worker.
    * Sends an `NothingToDo` reply if there is currently nothing to do.
    */
  private def tryAssignJob(worker: ActorRef): Unit = {
    if (queue.isEmpty) {
      log.info("Currently nothing to do for " + worker.path.name)
      worker ! NoJobsAvailable
    } else if (!assignedJobs.contains(worker)) {
      log.error(
        "Attempted to assign job to unregistered worker " + 
        worker.path.name
      )
    } else if (assignedJobs(worker).isEmpty){
      assert(!queue.isEmpty, "Queue should not be empty, size = " + queue.size)
      val internalJob = queue.dequeue
      assert(!(internalJob == null), "Dequeued job should be not null")
      assignedJobs(worker) = Some(internalJob)
      sendJobToWorker(internalJob, worker)
      log.info(
        "Assigned job " + internalJob.job + 
        " to " + worker.path.name
      )
    } else {
      log.info(
        "Worker is already occupied, " +
        "no new job assignment for " + 
        worker.path.name
      )
    }
  }

  /** Puts a job of a failed worker back into the queue
    */
  private def withdrawJob(worker: ActorRef): Unit = {
    if (!assignedJobs.contains(worker)) {
      log.error(
        "Attempted to withdraw a job " +
        "from a non-registered Worker {}", 
        worker.path.name
      )
    } else {
      assignedJobs(worker) match {
        case None => // do nothing
          log.info("Tried to withdraw job from " + worker + 
            " (nothing to withdraw)")
        case Some(oldJob) =>
          log.info("Withdrawing and re-enqueueing job from " + worker)
          enqueueSimple(oldJob.label, oldJob.job)
          assignedJobs(worker) = None
      }
    }
  }
  
  /** Returns collection with all idle workers
    */
  private def idleWorkers = 
    for ((w, None) <- assignedJobs) yield w
    
  /** Returns collection of all workers
    */
  private def allWorkers = assignedJobs.keySet
  
  /** Handles reminders sent after the initialization phase
    */
  protected def handleReminders: Receive = {
    // after we switch into normal operation mode, we should 
    // assign a job to all idle workers that joined the master
    // in the initialization phase
    case r: Reminder if (isRelevant(r)) =>
      val ws = idleWorkers
      log.info(
        s"Trying to assign ${queue.size} jobs from initialization phase " +
        s"to ${ws.size} workers: { " + ws.mkString(",") +" }" 
      )
      for (w <- ws) {
        tryAssignJob(w)
      }
    case r: Reminder => { /* no longer relevant, ignore */ }
  }
  
  /** Behavior for normal operation mode.
    *
    * Trying to assign jobs to workers,
    * withdrawing jobs from terminated workers.
    */
  protected[master] def handleWorkerRequests: Receive = {
    case WorkerHere =>
      log.info("Got job request from a worker " + sender.path.name)
      register(sender)
      tryAssignJob(sender)
      
    case Terminated(worker) if (assignedJobs.contains(worker)) => 
        withdrawJob(worker)

    case RemoteNodeNotResponding(worker) if(assignedJobs.contains(worker)) =>
      withdrawJob(worker)
  }
  
  /** Handles results from workers
    */
  protected[master] def handleWorkerResponses: Receive = {
    case InternalResult(label, result) => {
      val logMessageIntro = "Received result " + label + " from " + 
        sender.path.name + " "
      assignedJobs(sender) match {
        case None => log.error(
          logMessageIntro + " but there were no jobs assigned to this worker"
        )
        case Some(originalJob) => {
          if (originalJob.label != label) {
            log.error(
              logMessageIntro + " but the id was wrong: original = " + 
              originalJob.label + " returned = " + label
            )
            withdrawJob(sender)
          } else {
            log.info(
              logMessageIntro + 
              ", fulfilling promise, try assign new job. "
            )
            fulfillPromise(label, result)
            assignedJobs(sender) = None
            tryAssignJob(sender)
          }
        }
      }
    }
  } 
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy