
com.akkaserverless.javasdk.impl.eventsourcedentity.EventSourcedEntitiesImpl.scala Maven / Gradle / Ivy
/*
* Copyright 2021 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.akkaserverless.javasdk.impl.eventsourcedentity
import akka.NotUsed
import akka.actor.ActorSystem
import akka.stream.scaladsl.Flow
import akka.stream.scaladsl.Source
import com.akkaserverless.javasdk.AkkaServerlessRunner.Configuration
import com.akkaserverless.javasdk.eventsourcedentity._
import com.akkaserverless.javasdk.impl._
import com.akkaserverless.javasdk.impl.effect.EffectSupport
import com.akkaserverless.javasdk.impl.effect.ErrorReplyImpl
import com.akkaserverless.javasdk.impl.effect.MessageReplyImpl
import com.akkaserverless.javasdk.impl.effect.SecondaryEffectImpl
import com.akkaserverless.javasdk.impl.eventsourcedentity.EventSourcedEntityRouter.CommandResult
import com.akkaserverless.javasdk.Context
import com.akkaserverless.javasdk.Metadata
import com.akkaserverless.protocol.event_sourced_entity.EventSourcedStreamIn.Message.{ Init => InInit }
import com.akkaserverless.protocol.event_sourced_entity.EventSourcedStreamIn.Message.{ Empty => InEmpty }
import com.akkaserverless.protocol.event_sourced_entity.EventSourcedStreamIn.Message.{ Command => InCommand }
import com.akkaserverless.protocol.event_sourced_entity.EventSourcedStreamIn.Message.{ Event => InEvent }
import com.akkaserverless.protocol.event_sourced_entity.EventSourcedStreamIn.Message.{
SnapshotRequest => InSnapshotRequest
}
import com.akkaserverless.protocol.event_sourced_entity.EventSourcedStreamOut.Message.{ Reply => OutReply }
import com.akkaserverless.protocol.event_sourced_entity.EventSourcedStreamOut.Message.{ Failure => OutFailure }
import com.akkaserverless.protocol.event_sourced_entity.EventSourcedStreamOut.Message.{
SnapshotReply => OutSnapshotReply
}
import com.akkaserverless.protocol.event_sourced_entity._
import com.google.protobuf.any.{ Any => ScalaPbAny }
import com.google.protobuf.Descriptors
import scala.util.control.NonFatal
import com.akkaserverless.javasdk.impl.EventSourcedEntityFactory
import com.akkaserverless.protocol.component.Failure
import org.slf4j.LoggerFactory
final class EventSourcedEntityService(
val factory: EventSourcedEntityFactory,
override val descriptor: Descriptors.ServiceDescriptor,
val anySupport: AnySupport,
override val entityType: String,
val snapshotEvery: Int, // FIXME remove and only use entityOptions snapshotEvery?
val entityOptions: Option[EventSourcedEntityOptions])
extends Service {
def this(
factory: EventSourcedEntityFactory,
descriptor: Descriptors.ServiceDescriptor,
anySupport: AnySupport,
entityType: String,
snapshotEvery: Int,
entityOptions: EventSourcedEntityOptions) =
this(factory, descriptor, anySupport, entityType, snapshotEvery, Some(entityOptions))
override def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]] =
factory match {
case resolved: ResolvedEntityFactory => Some(resolved.resolvedMethods)
case _ => None
}
override final val componentType = EventSourcedEntities.name
def withSnapshotEvery(snapshotEvery: Int): EventSourcedEntityService =
if (snapshotEvery != this.snapshotEvery)
new EventSourcedEntityService(
this.factory,
this.descriptor,
this.anySupport,
this.entityType,
snapshotEvery,
this.entityOptions)
else
this
override def componentOptions: Option[ComponentOptions] = entityOptions
}
final class EventSourcedEntitiesImpl(
system: ActorSystem,
_services: Map[String, EventSourcedEntityService],
configuration: Configuration)
extends EventSourcedEntities {
import EntityExceptions._
private val log = LoggerFactory.getLogger(this.getClass)
private final val services = _services.iterator.map { case (name, service) =>
if (service.snapshotEvery < 0)
log.warn("Snapshotting disabled for entity [{}], this is not recommended.", service.entityType)
// FIXME overlay configuration provided by _system
(name, if (service.snapshotEvery == 0) service.withSnapshotEvery(configuration.snapshotEvery) else service)
}.toMap
/**
* The stream. One stream will be established per active entity. Once established, the first message sent will be
* Init, which contains the entity ID, and, if the entity has previously persisted a snapshot, it will contain that
* snapshot. It will then send zero to many event messages, one for each event previously persisted. The entity is
* expected to apply these to its state in a deterministic fashion. Once all the events are sent, one to many commands
* are sent, with new commands being sent as new requests for the entity come in. The entity is expected to reply to
* each command with exactly one reply message. The entity should reply in order, and any events that the entity
* requests to be persisted the entity should handle itself, applying them to its own state, as if they had arrived as
* events when the event stream was being replayed on load.
*/
override def handle(in: akka.stream.scaladsl.Source[EventSourcedStreamIn, akka.NotUsed])
: akka.stream.scaladsl.Source[EventSourcedStreamOut, akka.NotUsed] =
in.prefixAndTail(1)
.flatMapConcat {
case (Seq(EventSourcedStreamIn(InInit(init), _)), source) =>
source.via(runEntity(init))
case (Seq(), _) =>
// if error during recovery in proxy the stream will be completed before init
log.error("Event Sourced Entity stream closed before init.")
Source.empty[EventSourcedStreamOut]
case (Seq(EventSourcedStreamIn(other, _)), _) =>
throw ProtocolException(
s"Expected init message for Event Sourced Entity, but received [${other.getClass.getName}]")
}
.recover { case error =>
// only "unexpected" exceptions should end up here
ErrorHandling.withCorrelationId { correlationId =>
log.error(failureMessageForLog(error), error)
EventSourcedStreamOut(OutFailure(Failure(description = s"Unexpected failure [$correlationId]")))
}
}
private def runEntity(init: EventSourcedInit): Flow[EventSourcedStreamIn, EventSourcedStreamOut, NotUsed] = {
val service =
services.getOrElse(init.serviceName, throw ProtocolException(init, s"Service not found: ${init.serviceName}"))
val handler = service.factory
.create(new EventSourcedEntityContextImpl(init.entityId))
.asInstanceOf[EventSourcedEntityRouter[Any, EventSourcedEntity[Any]]]
val thisEntityId = init.entityId
val startingSequenceNumber = (for {
snapshot <- init.snapshot
any <- snapshot.snapshot
} yield {
val snapshotSequence = snapshot.snapshotSequence
handler._internalHandleSnapshot(service.anySupport.decodeMessage(any))
snapshotSequence
}).getOrElse(0L)
Flow[EventSourcedStreamIn]
.map(_.message)
.scan[(Long, Option[EventSourcedStreamOut.Message])]((startingSequenceNumber, None)) {
case (_, InEvent(event)) =>
// Note that these only come on replay
val context = new EventContextImpl(thisEntityId, event.sequence)
val ev =
service.anySupport
.decodeMessage(event.payload.get)
.asInstanceOf[AnyRef] // FIXME empty?
handler._internalHandleEvent(ev, context)
(event.sequence, None)
case ((sequence, _), InCommand(command)) =>
if (thisEntityId != command.entityId)
throw ProtocolException(command, "Receiving entity is not the intended recipient of command")
val cmd =
service.anySupport.decodeMessage(
command.payload.getOrElse(throw ProtocolException(command, "No command payload")))
val metadata = new MetadataImpl(command.metadata.map(_.entries.toVector).getOrElse(Nil))
val context =
new CommandContextImpl(thisEntityId, sequence, command.name, command.id, metadata)
val CommandResult(
events: Vector[Any],
secondaryEffect: SecondaryEffectImpl,
snapshot: Option[Any],
endSequenceNumber) =
try {
handler._internalHandleCommand(
command.name,
cmd,
context,
service.snapshotEvery,
seqNr => new EventContextImpl(thisEntityId, seqNr))
} catch {
case e: EntityException => throw e
case NonFatal(error) =>
throw EntityException(command, s"Unexpected failure: $error", Some(error))
} finally {
context.deactivate() // Very important!
}
val serializedSecondaryEffect = secondaryEffect match {
case MessageReplyImpl(message, metadata, sideEffects) =>
MessageReplyImpl(service.anySupport.encodeJava(message), metadata, sideEffects)
case other => other
}
val clientAction =
serializedSecondaryEffect.replyToClientAction(service.anySupport, command.id, allowNoReply = false)
serializedSecondaryEffect match {
case error: ErrorReplyImpl[_] =>
(
endSequenceNumber,
Some(OutReply(EventSourcedReply(commandId = command.id, clientAction = clientAction))))
case _ => // non-error
val serializedEvents = events.map(event => ScalaPbAny.fromJavaProto(service.anySupport.encodeJava(event)))
val serializedSnapshot =
snapshot.map(state => ScalaPbAny.fromJavaProto(service.anySupport.encodeJava(state)))
(
endSequenceNumber,
Some(
OutReply(
EventSourcedReply(
command.id,
clientAction,
EffectSupport.sideEffectsFrom(service.anySupport, serializedSecondaryEffect),
serializedEvents,
serializedSnapshot))))
}
case ((sequence, _), InSnapshotRequest(request)) =>
val reply =
EventSourcedSnapshotReply(request.requestId, Some(service.anySupport.encodeScala(handler._stateOrEmpty())))
(sequence, Some(OutSnapshotReply(reply)))
case (_, InInit(_)) =>
throw ProtocolException(init, "Entity already inited")
case (_, InEmpty) =>
throw ProtocolException(init, "Received empty/unknown message")
}
.collect { case (_, Some(message)) =>
EventSourcedStreamOut(message)
}
.recover { case error =>
// only "unexpected" exceptions should end up here
ErrorHandling.withCorrelationId { correlationId =>
LoggerFactory.getLogger(handler.entityClass).error(failureMessageForLog(error), error)
EventSourcedStreamOut(OutFailure(Failure(description = s"Unexpected failure [$correlationId]")))
}
}
}
private class CommandContextImpl(
override val entityId: String,
override val sequenceNumber: Long,
override val commandName: String,
override val commandId: Long,
override val metadata: Metadata)
extends AbstractContext(system)
with CommandContext
with ActivatableContext
private class EventSourcedEntityContextImpl(override final val entityId: String)
extends AbstractContext(system)
with EventSourcedEntityContext
private final class EventContextImpl(entityId: String, override val sequenceNumber: Long)
extends EventSourcedEntityContextImpl(entityId)
with EventContext
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy