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

akka.projection.eventsourced.scaladsl.EventSourcedProvider.scala Maven / Gradle / Ivy

There is a newer version: 1.5.0-M4
Show newest version
/*
 * Copyright (C) 2020-2023 Lightbend Inc. 
 */

package akka.projection.eventsourced.scaladsl

import java.time.Instant

import scala.collection.immutable
import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import akka.NotUsed
import akka.actor.typed.ActorSystem
import akka.persistence.query.NoOffset
import akka.persistence.query.Offset
import akka.persistence.query.PersistenceQuery
import akka.persistence.query.scaladsl.EventsByTagQuery
import akka.persistence.query.scaladsl.ReadJournal
import akka.persistence.query.typed.scaladsl.EventTimestampQuery
import akka.persistence.query.typed.scaladsl.EventsBySliceQuery
import akka.persistence.query.typed.scaladsl.EventsBySliceStartingFromSnapshotsQuery
import akka.persistence.query.typed.scaladsl.LoadEventQuery
import akka.projection.BySlicesSourceProvider
import akka.projection.eventsourced.EventEnvelope
import akka.projection.internal.CanTriggerReplay
import akka.projection.scaladsl.SourceProvider
import akka.stream.scaladsl.Source

object EventSourcedProvider {

  def eventsByTag[Event](
      system: ActorSystem[_],
      readJournalPluginId: String,
      tag: String): SourceProvider[Offset, EventEnvelope[Event]] = {
    val eventsByTagQuery =
      PersistenceQuery(system).readJournalFor[EventsByTagQuery](readJournalPluginId)
    eventsByTag(system, eventsByTagQuery, tag)
  }

  def eventsByTag[Event](
      system: ActorSystem[_],
      eventsByTagQuery: EventsByTagQuery,
      tag: String): SourceProvider[Offset, EventEnvelope[Event]] = {
    new EventsByTagSourceProvider(system, eventsByTagQuery, tag)
  }

  private class EventsByTagSourceProvider[Event](
      system: ActorSystem[_],
      eventsByTagQuery: EventsByTagQuery,
      tag: String)
      extends SourceProvider[Offset, EventEnvelope[Event]] {
    implicit val executionContext: ExecutionContext = system.executionContext

    override def source(offset: () => Future[Option[Offset]]): Future[Source[EventEnvelope[Event], NotUsed]] =
      offset().map { offsetOpt =>
        val offset = offsetOpt.getOrElse(NoOffset)
        eventsByTagQuery
          .eventsByTag(tag, offset)
          .map(env => EventEnvelope(env))
      }

    override def extractOffset(envelope: EventEnvelope[Event]): Offset = envelope.offset

    override def extractCreationTime(envelope: EventEnvelope[Event]): Long = envelope.timestamp
  }

  def eventsBySlices[Event](
      system: ActorSystem[_],
      readJournalPluginId: String,
      entityType: String,
      minSlice: Int,
      maxSlice: Int): SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    val eventsBySlicesQuery =
      PersistenceQuery(system).readJournalFor[EventsBySliceQuery](readJournalPluginId)
    eventsBySlices(system, eventsBySlicesQuery, entityType, minSlice, maxSlice)
  }

  /**
   * By default, the `SourceProvider` uses the stored offset when starting the Projection. This offset can be adjusted
   * by defining the `adjustStartOffset` function, which is a function from loaded offset (if any) to the
   * adjusted offset that will be used to by the `eventsBySlicesQuery`.
   */
  def eventsBySlices[Event](
      system: ActorSystem[_],
      readJournalPluginId: String,
      entityType: String,
      minSlice: Int,
      maxSlice: Int,
      adjustStartOffset: Option[Offset] => Future[Option[Offset]])
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    val eventsBySlicesQuery =
      PersistenceQuery(system).readJournalFor[EventsBySliceQuery](readJournalPluginId)
    eventsBySlices(system, eventsBySlicesQuery, entityType, minSlice, maxSlice, adjustStartOffset)
  }

  def eventsBySlices[Event](
      system: ActorSystem[_],
      eventsBySlicesQuery: EventsBySliceQuery,
      entityType: String,
      minSlice: Int,
      maxSlice: Int): SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    eventsBySlices(system, eventsBySlicesQuery, entityType, minSlice, maxSlice, offset => Future.successful(offset))
  }

  /**
   * By default, the `SourceProvider` uses the stored offset when starting the Projection. This offset can be adjusted
   * by defining the `adjustStartOffset` function, which is a function from loaded offset (if any) to the
   * adjusted offset that will be used to by the `eventsBySlicesQuery`.
   */
  def eventsBySlices[Event](
      system: ActorSystem[_],
      eventsBySlicesQuery: EventsBySliceQuery,
      entityType: String,
      minSlice: Int,
      maxSlice: Int,
      adjustStartOffset: Option[Offset] => Future[Option[Offset]])
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    eventsBySlicesQuery match {
      case query: EventsBySliceQuery with CanTriggerReplay =>
        new EventsBySlicesSourceProvider[Event](
          system,
          eventsBySlicesQuery,
          entityType,
          minSlice,
          maxSlice,
          adjustStartOffset) with CanTriggerReplay {
          override private[akka] def triggerReplay(persistenceId: String, fromSeqNr: Long): Unit =
            query.triggerReplay(persistenceId, fromSeqNr)
        }
      case _ =>
        new EventsBySlicesSourceProvider(system, eventsBySlicesQuery, entityType, minSlice, maxSlice, adjustStartOffset)
    }
  }

  def eventsBySlicesStartingFromSnapshots[Snapshot, Event](
      system: ActorSystem[_],
      readJournalPluginId: String,
      entityType: String,
      minSlice: Int,
      maxSlice: Int,
      transformSnapshot: Snapshot => Event)
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    val eventsBySlicesQuery =
      PersistenceQuery(system).readJournalFor[EventsBySliceStartingFromSnapshotsQuery](readJournalPluginId)
    eventsBySlicesStartingFromSnapshots(system, eventsBySlicesQuery, entityType, minSlice, maxSlice, transformSnapshot)
  }

  /**
   * By default, the `SourceProvider` uses the stored offset when starting the Projection. This offset can be adjusted
   * by defining the `adjustStartOffset` function, which is a function from loaded offset (if any) to the
   * adjusted offset that will be used to by the `eventsBySlicesQuery`.
   */
  def eventsBySlicesStartingFromSnapshots[Snapshot, Event](
      system: ActorSystem[_],
      readJournalPluginId: String,
      entityType: String,
      minSlice: Int,
      maxSlice: Int,
      transformSnapshot: Snapshot => Event,
      adjustStartOffset: Option[Offset] => Future[Option[Offset]])
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    val eventsBySlicesQuery =
      PersistenceQuery(system).readJournalFor[EventsBySliceStartingFromSnapshotsQuery](readJournalPluginId)
    eventsBySlicesStartingFromSnapshots(
      system,
      eventsBySlicesQuery,
      entityType,
      minSlice,
      maxSlice,
      transformSnapshot,
      adjustStartOffset)
  }

  def eventsBySlicesStartingFromSnapshots[Snapshot, Event](
      system: ActorSystem[_],
      eventsBySlicesQuery: EventsBySliceStartingFromSnapshotsQuery,
      entityType: String,
      minSlice: Int,
      maxSlice: Int,
      transformSnapshot: Snapshot => Event): SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] =
    eventsBySlicesStartingFromSnapshots(
      system,
      eventsBySlicesQuery,
      entityType,
      minSlice,
      maxSlice,
      transformSnapshot,
      offset => Future.successful(offset))

  /**
   * By default, the `SourceProvider` uses the stored offset when starting the Projection. This offset can be adjusted
   * by defining the `adjustStartOffset` function, which is a function from loaded offset (if any) to the
   * adjusted offset that will be used to by the `eventsBySlicesQuery`.
   */
  def eventsBySlicesStartingFromSnapshots[Snapshot, Event](
      system: ActorSystem[_],
      eventsBySlicesQuery: EventsBySliceStartingFromSnapshotsQuery,
      entityType: String,
      minSlice: Int,
      maxSlice: Int,
      transformSnapshot: Snapshot => Event,
      adjustStartOffset: Option[Offset] => Future[Option[Offset]])
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    eventsBySlicesQuery match {
      case query: EventsBySliceStartingFromSnapshotsQuery with CanTriggerReplay =>
        new EventsBySlicesStartingFromSnapshotsSourceProvider[Snapshot, Event](
          system,
          eventsBySlicesQuery,
          entityType,
          minSlice,
          maxSlice,
          transformSnapshot,
          adjustStartOffset) with CanTriggerReplay {
          override private[akka] def triggerReplay(persistenceId: String, fromSeqNr: Long): Unit =
            query.triggerReplay(persistenceId, fromSeqNr)
        }
      case _ =>
        new EventsBySlicesStartingFromSnapshotsSourceProvider(
          system,
          eventsBySlicesQuery,
          entityType,
          minSlice,
          maxSlice,
          transformSnapshot,
          adjustStartOffset)
    }
  }

  def sliceForPersistenceId(system: ActorSystem[_], readJournalPluginId: String, persistenceId: String): Int =
    PersistenceQuery(system)
      .readJournalFor[EventsBySliceQuery](readJournalPluginId)
      .sliceForPersistenceId(persistenceId)

  def sliceRanges(system: ActorSystem[_], readJournalPluginId: String, numberOfRanges: Int): immutable.Seq[Range] =
    PersistenceQuery(system).readJournalFor[EventsBySliceQuery](readJournalPluginId).sliceRanges(numberOfRanges)

  private class EventsBySlicesSourceProvider[Event](
      system: ActorSystem[_],
      eventsBySlicesQuery: EventsBySliceQuery,
      entityType: String,
      override val minSlice: Int,
      override val maxSlice: Int,
      adjustStartOffset: Option[Offset] => Future[Option[Offset]])
      extends SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]]
      with BySlicesSourceProvider
      with EventTimestampQuerySourceProvider
      with LoadEventQuerySourceProvider {
    implicit val executionContext: ExecutionContext = system.executionContext

    override def readJournal: ReadJournal = eventsBySlicesQuery

    override def source(offset: () => Future[Option[Offset]])
        : Future[Source[akka.persistence.query.typed.EventEnvelope[Event], NotUsed]] = {
      for {
        storedOffset <- offset()
        startOffset <- adjustStartOffset(storedOffset)
      } yield {
        eventsBySlicesQuery.eventsBySlices(entityType, minSlice, maxSlice, startOffset.getOrElse(NoOffset))
      }
    }

    override def extractOffset(envelope: akka.persistence.query.typed.EventEnvelope[Event]): Offset = envelope.offset

    override def extractCreationTime(envelope: akka.persistence.query.typed.EventEnvelope[Event]): Long =
      envelope.timestamp

  }

  private class EventsBySlicesStartingFromSnapshotsSourceProvider[Snapshot, Event](
      system: ActorSystem[_],
      eventsBySlicesQuery: EventsBySliceStartingFromSnapshotsQuery,
      entityType: String,
      override val minSlice: Int,
      override val maxSlice: Int,
      transformSnapshot: Snapshot => Event,
      adjustStartOffset: Option[Offset] => Future[Option[Offset]])
      extends SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]]
      with BySlicesSourceProvider
      with EventTimestampQuerySourceProvider
      with LoadEventQuerySourceProvider {
    implicit val executionContext: ExecutionContext = system.executionContext

    override def readJournal: ReadJournal = eventsBySlicesQuery

    override def source(offset: () => Future[Option[Offset]])
        : Future[Source[akka.persistence.query.typed.EventEnvelope[Event], NotUsed]] = {
      for {
        storedOffset <- offset()
        startOffset <- adjustStartOffset(storedOffset)
      } yield {
        eventsBySlicesQuery.eventsBySlicesStartingFromSnapshots(
          entityType,
          minSlice,
          maxSlice,
          startOffset.getOrElse(NoOffset),
          transformSnapshot)
      }
    }

    override def extractOffset(envelope: akka.persistence.query.typed.EventEnvelope[Event]): Offset = envelope.offset

    override def extractCreationTime(envelope: akka.persistence.query.typed.EventEnvelope[Event]): Long =
      envelope.timestamp

  }

  private trait EventTimestampQuerySourceProvider extends EventTimestampQuery {
    def readJournal: ReadJournal

    override def timestampOf(persistenceId: String, sequenceNr: Long): Future[Option[Instant]] =
      readJournal match {
        case timestampQuery: EventTimestampQuery =>
          timestampQuery.timestampOf(persistenceId, sequenceNr)
        case _ =>
          Future.failed(
            new IllegalStateException(
              s"[${readJournal.getClass.getName}] must implement [${classOf[EventTimestampQuery].getName}]"))
      }
  }

  private trait LoadEventQuerySourceProvider extends LoadEventQuery {
    def readJournal: ReadJournal

    override def loadEnvelope[Evt](
        persistenceId: String,
        sequenceNr: Long): Future[akka.persistence.query.typed.EventEnvelope[Evt]] =
      readJournal match {
        case laodEventQuery: LoadEventQuery =>
          laodEventQuery.loadEnvelope(persistenceId, sequenceNr)
        case _ =>
          Future.failed(
            new IllegalStateException(
              s"[${readJournal.getClass.getName}] must implement [${classOf[LoadEventQuery].getName}]"))
      }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy