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

akka.projection.eventsourced.javadsl.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.javadsl

import java.time.Instant
import java.util.Optional
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.function.Supplier
import java.util.function.{ Function => JFunction }

import scala.annotation.nowarn

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

object EventSourcedProvider {

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

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

  /**
   * INTERNAL API
   */
  @InternalApi
  @nowarn("msg=never used") // system
  private class EventsByTagSourceProvider[Event](
      system: ActorSystem[_],
      eventsByTagQuery: EventsByTagQuery,
      tag: String)
      extends javadsl.SourceProvider[Offset, EventEnvelope[Event]] {

    override def source(offsetAsync: Supplier[CompletionStage[Optional[Offset]]])
        : CompletionStage[Source[EventEnvelope[Event], NotUsed]] = {
      offsetAsync.get().thenApply { storedOffset =>
        eventsByTagQuery
          .eventsByTag(tag, storedOffset.orElse(NoOffset))
          .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).getReadJournalFor(classOf[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: JFunction[Optional[Offset], CompletionStage[Optional[Offset]]])
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    val eventsBySlicesQuery =
      PersistenceQuery(system).getReadJournalFor(classOf[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: Optional[Offset]) => CompletableFuture.completedFuture(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: JFunction[Optional[Offset], CompletionStage[Optional[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 {

          private[akka] override 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: java.util.function.Function[Snapshot, Event])
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    val eventsBySlicesQuery =
      PersistenceQuery(system).getReadJournalFor(classOf[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: java.util.function.Function[Snapshot, Event],
      adjustStartOffset: JFunction[Optional[Offset], CompletionStage[Optional[Offset]]])
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    val eventsBySlicesQuery =
      PersistenceQuery(system).getReadJournalFor(classOf[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: java.util.function.Function[Snapshot, Event])
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] =
    eventsBySlicesStartingFromSnapshots(
      system,
      eventsBySlicesQuery,
      entityType,
      minSlice,
      maxSlice,
      transformSnapshot,
      (offset: Optional[Offset]) => CompletableFuture.completedFuture(offset))

  def eventsBySlicesStartingFromSnapshots[Snapshot, Event](
      system: ActorSystem[_],
      eventsBySlicesQuery: EventsBySliceStartingFromSnapshotsQuery,
      entityType: String,
      minSlice: Int,
      maxSlice: Int,
      transformSnapshot: java.util.function.Function[Snapshot, Event],
      adjustStartOffset: JFunction[Optional[Offset], CompletionStage[Optional[Offset]]])
      : SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]] = {
    eventsBySlicesQuery match {
      case query: EventsBySliceQuery with CanTriggerReplay =>
        new EventsBySlicesStartingFromSnapshotsSourceProvider[Snapshot, Event](
          system,
          eventsBySlicesQuery,
          entityType,
          minSlice,
          maxSlice,
          transformSnapshot,
          adjustStartOffset) with CanTriggerReplay {

          private[akka] override 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)
      .getReadJournalFor(classOf[EventsBySliceQuery], readJournalPluginId)
      .sliceForPersistenceId(persistenceId)

  def sliceRanges(
      system: ActorSystem[_],
      readJournalPluginId: String,
      numberOfRanges: Int): java.util.List[Pair[Integer, Integer]] =
    PersistenceQuery(system)
      .getReadJournalFor(classOf[EventsBySliceQuery], readJournalPluginId)
      .sliceRanges(numberOfRanges)

  /**
   * INTERNAL API
   */
  @InternalApi
  @nowarn("msg=never used") // system
  private class EventsBySlicesSourceProvider[Event](
      system: ActorSystem[_],
      eventsBySlicesQuery: EventsBySliceQuery,
      entityType: String,
      override val minSlice: Int,
      override val maxSlice: Int,
      adjustStartOffset: JFunction[Optional[Offset], CompletionStage[Optional[Offset]]])
      extends SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]]
      with BySlicesSourceProvider
      with EventTimestampQuerySourceProvider
      with LoadEventQuerySourceProvider {

    override def readJournal: ReadJournal = eventsBySlicesQuery

    override def source(offsetAsync: Supplier[CompletionStage[Optional[Offset]]])
        : CompletionStage[Source[akka.persistence.query.typed.EventEnvelope[Event], NotUsed]] = {
      offsetAsync.get().thenCompose { storedOffset =>
        adjustStartOffset(storedOffset).thenApply { startOffset =>
          eventsBySlicesQuery
            .eventsBySlices(entityType, minSlice, maxSlice, startOffset.orElse(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

  }

  /**
   * INTERNAL API
   */
  @InternalApi
  @nowarn("msg=never used") // system
  private class EventsBySlicesStartingFromSnapshotsSourceProvider[Snapshot, Event](
      system: ActorSystem[_],
      eventsBySlicesQuery: EventsBySliceStartingFromSnapshotsQuery,
      entityType: String,
      override val minSlice: Int,
      override val maxSlice: Int,
      transformSnapshot: java.util.function.Function[Snapshot, Event],
      adjustStartOffset: JFunction[Optional[Offset], CompletionStage[Optional[Offset]]])
      extends SourceProvider[Offset, akka.persistence.query.typed.EventEnvelope[Event]]
      with BySlicesSourceProvider
      with EventTimestampQuerySourceProvider
      with LoadEventQuerySourceProvider {

    override def readJournal: ReadJournal = eventsBySlicesQuery

    override def source(offsetAsync: Supplier[CompletionStage[Optional[Offset]]])
        : CompletionStage[Source[akka.persistence.query.typed.EventEnvelope[Event], NotUsed]] = {
      offsetAsync.get().thenCompose { storedOffset =>
        adjustStartOffset(storedOffset).thenApply { startOffset =>
          eventsBySlicesQuery
            .eventsBySlicesStartingFromSnapshots(
              entityType,
              minSlice,
              maxSlice,
              startOffset.orElse(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

  }

  /**
   * INTERNAL API
   */
  @InternalApi
  private trait EventTimestampQuerySourceProvider extends EventTimestampQuery {
    def readJournal: ReadJournal

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

  /**
   * INTERNAL API
   */
  @InternalApi
  private trait LoadEventQuerySourceProvider extends LoadEventQuery {
    def readJournal: ReadJournal

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




© 2015 - 2025 Weber Informatics LLC | Privacy Policy