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

com.seancheatham.akka.persistence.FirebaseJournal.scala Maven / Gradle / Ivy

The newest version!
package com.seancheatham.akka.persistence

import java.util.concurrent.Executors

import akka.actor.{ActorLogging, ExtendedActorSystem}
import akka.persistence.journal.AsyncWriteJournal
import akka.persistence.{AtomicWrite, PersistentRepr}
import akka.serialization.{Serialization, SerializationExtension}
import com.google.firebase.database.{DataSnapshot, DatabaseError, ValueEventListener}
import com.seancheatham.akka.persistence.FirebaseJournal.EventItem
import com.seancheatham.storage.firebase.FirebaseDatabase
import com.typesafe.config.Config
import play.api.libs.json._

import scala.collection.immutable.Seq
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
import scala.util.{Success, Try}

/**
  * An Akka Persistence Journaling system which is backed by a Google Firebase instance.  Firebase provides several
  * features, and among them is a Realtime Database.  Their database is document-based, meaning it is effectively
  * modeled as a giant JSON object, thus allowing for an expressive way of interacting with it.  The true power, however,
  * comes from its "realtime"-ness.  Firebase is designed for high-performance reads and writes, as well as for listening
  * on real-time connections.  This makes it effective for Akka Journaling.
  *
  * The configuration settings defined for this plugin are:
  * {
  * base_key_path = "infrastructure/akka/persistence"
  * }
  */
class FirebaseJournal(config: Config) extends AsyncWriteJournal with ActorLogging {

  /**
    * Akka documentation doesn't recommend using the actor system's execution context,
    * so create a custom one for this journal.
    */
  private implicit val ec =
    ExecutionContext.fromExecutor(
      Executors.newFixedThreadPool(2)
    )

  /**
    * The base path in within the context of Firebase to write Actor journals.  For example, events for persistence ID "X"
    * will be written to `{persistenceKeyPath}/X/events`
    */
  val persistenceKeyPath: String =
    config.getString("base_key_path")

  /**
    * The ActorSystem to which this journal's Actor belongs
    */
  private implicit val actorSystem =
    persistence.system

  /**
    * An implicit Serialization used when reading/writing events in Firebase
    */
  private implicit val serialization: Serialization =
    SerializationExtension(actorSystem)

  /**
    * A Firebase DB instance
    */
  private val db =
    FirebaseDatabase.fromConfig(config)

  /**
    * Appends the given messages to the `{persistenceKeyPath}/{persistenceId}/events/{seqNr}` path
    */
  def asyncWriteMessages(messages: Seq[AtomicWrite]): Future[Seq[Try[Unit]]] = {
    def writeRepr(repr: PersistentRepr,
                  persistenceId: String) =
      db.write(
        persistenceKeyPath,
        persistenceId,
        "events",
        repr.sequenceNr.toString
      )(Json.toJson(EventItem(repr)))
        .map(_ => Success())

    Future.traverse(messages) {
      message =>
        val future =
          Future.traverse(message.payload)(writeRepr(_, message.persistenceId))
        future.onFailure {
          case _ =>
            import scala.concurrent.duration._
            Await.result(
              Future.traverse(message.payload.map(_.sequenceNr.toString))(
                db.delete(persistenceKeyPath, message.persistenceId, "events", _)
              ),
              3.seconds
            )
        }
        future.map(_ => Success())
    }
  }

  /**
    * Deletes the relevant messages in the `{persistenceKeyPath}/{persistenceId}/events/` path
    */
  def asyncDeleteMessagesTo(persistenceId: String, toSequenceNr: Long): Future[Unit] = {
    db.getChildKeys(persistenceKeyPath, persistenceId, "events")
      .map(
        _.map(_.toLong)
          .filter(_ <= toSequenceNr)
          .map(_.toString)
          .map(idx => db.merge(persistenceKeyPath, persistenceId, "events", idx, "d") _)
      )
      .flatMap(
        Future.traverse(_)(_.apply(JsBoolean(true)))
          .map(_ => ())
      )
  }

  /**
    * Reads the messages between the given sequence numbers in the `{persistenceKeyPath}/{persistenceId}/events/` path,
    * calling the callback for each message.
    */
  def asyncReplayMessages(persistenceId: String, fromSequenceNr: Long, toSequenceNr: Long, max: Long)
                         (recoveryCallback: (PersistentRepr) => Unit): Future[Unit] = {
    var replayedCount = 0L
    import com.seancheatham.storage.firebase.FirebaseDatabase.JsHelper
    db.getCollection(persistenceKeyPath, persistenceId, "events")
      .map(
        _.zipWithIndex
          .collect {
            case (v: JsObject, i) if i >= fromSequenceNr && i <= toSequenceNr =>
              v -> i
          }
      )
      .map(messages =>
        while (messages.hasNext && replayedCount < max) {
          val (v, i) =
            messages.next()
          v.toOption
            .map(_.as[EventItem].repr(i, persistenceId))
            .foreach {
              replayedCount += 1
              recoveryCallback(_)
            }
        }
      )
  }

  /**
    * Retrieves the latest event and its sequence number in `{persistenceKeyPath}/{persistenceId}/events/`
    */
  def asyncReadHighestSequenceNr(persistenceId: String, fromSequenceNr: Long): Future[Long] = {
    val promise =
      Promise[Long]()

    import FirebaseDatabase.KeyHelper

    db.database.getReference(
      Seq(persistenceKeyPath, persistenceId, "events").keyify
    ).limitToLast(1)
      .addListenerForSingleValueEvent(
        new ValueEventListener {
          def onDataChange(dataSnapshot: DataSnapshot): Unit = {
            val childrenIterator = dataSnapshot.getChildren.iterator()
            val highestId =
              if (childrenIterator.hasNext)
                childrenIterator.next().getKey.toLong
              else
                0l
            promise success highestId
          }

          def onCancelled(databaseError: DatabaseError): Unit =
            promise failure new IllegalStateException(databaseError.getMessage)
        }
      )
    promise.future
  }
}

object FirebaseJournal {

  /**
    * Events get stored using this structure, however the field names are shortened when exporting to JSON,
    * to save space and time.
    *
    * @param manifest   @see [[akka.persistence.PersistentRepr#manifest]]
    * @param deleted    @see [[akka.persistence.PersistentRepr#deleted]]
    * @param sender     @see [[akka.persistence.PersistentRepr#sender]]
    * @param writerUuid @see [[akka.persistence.PersistentRepr#writerUuid]]
    * @param value      The serialized value of the Repr's payload.  If the value is a primitive, it'll be stored as a Json
    *              primitive.  If the value is some sort of serializable object, the object gets serialized
    *                   and then Base64 Encoded.
    * @param valueType  A hint as to what the [[com.seancheatham.akka.persistence.FirebaseJournal.EventItem#value()]] is
    */
  case class EventItem(manifest: String,
                       deleted: Boolean,
                       sender: Option[String],
                       writerUuid: String,
                       value: JsValue,
                       valueType: String) {

    /**
      * Base64 decodes and deserializes this item's value
      *
      * @param serialization An Actor Serialization
      * @return an Any, per deserialization rules
      */
    def value(implicit serialization: Serialization): Any =
      serializedToAny(valueType, value)

    /**
      * Converts this EventItem into a Persistence Repr
      *
      * @param id            The [[akka.persistence.PersistentImpl#sequenceNr()]]
      * @param persistenceId The [[akka.persistence.PersistentImpl#persistenceId()]]
      * @param system        An implicit actor system, for deserializing
      * @param serialization The actor's serialization
      * @return a PersistenceRepr
      */
    def repr(id: Long,
             persistenceId: String)(implicit system: ExtendedActorSystem,
                                    serialization: Serialization): PersistentRepr =
      PersistentRepr(serializedToAny(valueType, value), id, persistenceId, manifest, deleted, sender.map(system.provider.resolveActorRef).orNull, writerUuid)

  }

  object EventItem {
    def apply(repr: PersistentRepr)(implicit system: ExtendedActorSystem,
                                    serialization: Serialization): EventItem = {
      val (vt, v) =
        anyToSerialized(repr.payload)
      EventItem(
        repr.manifest,
        repr.deleted,
        Option(repr.sender).map(Serialization.serializedActorPath),
        repr.writerUuid,
        v,
        vt
      )
    }
  }

  implicit val format: Format[EventItem] =
    Format[EventItem](
      Reads[EventItem](v =>
        JsSuccess(
          EventItem(
            (v \ "m").as[String],
            (v \ "d").asOpt[Boolean].getOrElse[Boolean](false),
            (v \ "s").asOpt[String],
            (v \ "w").as[String],
            (v \ "v").get,
            (v \ "vt").as[String]
          )
        )
      ),
      Writes[EventItem] {
        item =>
          val objItems =
            Map(
              "m" -> Json.toJson(item.manifest),
              "w" -> Json.toJson(item.writerUuid),
              "v" -> Json.toJson(item.value),
              "vt" -> Json.toJson(item.valueType)
            ) ++
              item.sender.map("s" -> JsString(_)) ++
              (if (item.deleted) Seq("d" -> JsBoolean(true)) else Seq.empty)
          JsObject(objItems)
      }
    )
  Json.format[EventItem]

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy