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

com.github.jeroenr.tepkin.MongoCursor.scala Maven / Gradle / Ivy

The newest version!
package com.github.jeroenr.tepkin

import akka.actor.{ActorLogging, ActorRef, Props}
import akka.pattern.{ask, pipe}
import akka.stream.actor.ActorPublisher
import akka.stream.actor.ActorPublisherMessage.{Cancel, Request}
import akka.util.Timeout
import com.github.jeroenr.bson.BsonDocument
import com.github.jeroenr.tepkin.TepkinMessage.{CursorClosed, CursorOpened, Fetch}
import com.github.jeroenr.tepkin.protocol.message.{GetMoreMessage, KillCursorsMessage, Message, Reply}

/**
 * Represents a cursor to read data from Mongo. This actor is an ActorPublisher so it can be passed directly to a
 * `akka.stream.scaladsl.Source`
 *
 * @param pool reference to a [[MongoPoolManager]] actor.
 * @param message The first message to be sent to a MongoDB instance. The cursor will be opened after this message.
 * @param extractor Extractor function for extracting ns, cursorID and firstBatch from the reply to the message.
 * @param batchMultiplier totalDemand is multiplied by this factor to calculate numberToReturn to be sent in a GetMore
 *                        message. If you are sure that your Sink is fast then try increasing this number to improve
 *                        performance.
 * @param timeout Timeout for receiving a reply for the first message.
 */
class MongoCursor(pool: ActorRef,
                  message: Message,
                  extractor: Reply => (String, Long, List[BsonDocument]), // ns, cursorID, firstBatch
                  batchMultiplier: Int,
                  timeout: Timeout)
  extends ActorPublisher[List[BsonDocument]]
  with ActorLogging {

  import context.dispatcher

  pool.?(message)(timeout).pipeTo(self).onFailure {
    case cause: Throwable => onErrorThenStop(cause)
  }

  override def receive: Receive = {
    case reply: Reply =>
      val (ns, cursorID, firstBatch) = extractor(reply)

      if (cursorID == 0 && firstBatch.isEmpty) {
        onCompleteThenStop()
      } else {
        // Notify pool manager about the opened cursor.
        if (cursorID != 0) {
          pool ! CursorOpened(cursorID)
        }

        if (totalDemand <= 0) {
          context.become(sleeping(ns, cursorID, firstBatch))
        } else if (reply.cursorID == 0) {
          onNext(firstBatch)
          onCompleteThenStop()
        } else {
          onNext(firstBatch)
          if (totalDemand > 0) {
            context.become(fetching(ns, cursorID))
            self ! Fetch
          } else {
            context.become(sleeping(ns, cursorID, Nil))
          }
        }
      }
  }

  def sleeping(ns: String, cursorID: Long, buffer: List[BsonDocument]): Receive = {
    case request@Request(_) =>
      log.debug("Received {}", request)

      if (buffer.nonEmpty) {
        onNext(buffer)
      }

      if (cursorID == 0) {
        onCompleteThenStop()
      } else {
        if (totalDemand > 0) {
          context.become(fetching(ns, cursorID))
          self ! Fetch
        } else {
          context.become(sleeping(ns, cursorID, Nil))
        }
      }

    case Cancel =>
      killCursor(cursorID)
  }

  def fetching(ns: String, cursorID: Long): Receive = {
    case reply: Reply =>
      log.debug("Received Reply. numberReturned: {} , cursorID: {}", reply.numberReturned, reply.cursorID)

      if (reply.numberReturned > 0) {
        onNext(reply.documents)
      }

      if (reply.cursorID == 0) {
        onCompleteThenStop()
        log.debug("Cursor[{}] is totally read", cursorID)
      } else if (totalDemand > 0) {
        self ! Fetch
      } else {
        context.become(sleeping(ns, cursorID, Nil))
      }

    case Fetch =>
      log.debug("Received Fetch request")
      pool ! GetMoreMessage(ns, cursorID, totalDemand.toInt * batchMultiplier)

    case Cancel =>
      killCursor(cursorID)
  }

  private def killCursor(cursorID: Long): Unit = if (cursorID != 0) {
    log.debug("Killing cursor[{}]", cursorID)
    pool ! KillCursorsMessage(cursorID)
    pool ! CursorClosed(cursorID)
    context.stop(self)
  }
}

object MongoCursor {

  /**
   *
   * @param pool reference to a MongoPool actor.
   * @param message The first message to be sent to a MongoDB instance. The cursor will be opened after this message.
   * @param extractor Extractor function for extracting ns, cursorID and firstBatch from the reply to the message.
   * @param batchMultiplier totalDemand is multiplied by this factor to calculate numberToReturn to be sent in a GetMore
   *                        message. If you are sure that your Sink is fast then try increasing this number to improve
   *                        performance.
   * @param timeout Timeout for receiving a reply for the first message.
   */
  def props(pool: ActorRef,
            message: Message,
            extractor: Reply => (String, Long, List[BsonDocument]), // ns, cursorID, firstBatch
            batchMultiplier: Int,
            timeout: Timeout): Props = {
    Props(new MongoCursor(pool, message, extractor, batchMultiplier, timeout))
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy