@@ -20,6 +20,7 @@ import java.util.Optional
2020
2121import akka .NotUsed
2222import akka .actor .ActorSystem
23+ import akka .event .{Logging , LoggingAdapter }
2324import akka .stream .scaladsl .Flow
2425import com .google .protobuf .{Descriptors , Any => JavaPbAny }
2526import com .google .protobuf .any .{Any => ScalaPbAny }
@@ -35,14 +36,16 @@ import io.cloudstate.javasupport.impl.{
3536 ResolvedEntityFactory ,
3637 ResolvedServiceMethod
3738}
39+ import io .cloudstate .protocol .entity .{Command , Failure }
3840import io .cloudstate .protocol .event_sourced .EventSourcedStreamIn .Message .{
3941 Command => InCommand ,
4042 Empty => InEmpty ,
4143 Event => InEvent ,
4244 Init => InInit
4345}
44- import io .cloudstate .protocol .event_sourced .EventSourcedStreamOut .Message .{Reply => OutReply }
46+ import io .cloudstate .protocol .event_sourced .EventSourcedStreamOut .Message .{Failure => OutFailure , Reply => OutReply }
4547import io .cloudstate .protocol .event_sourced ._
48+ import scala .util .control .NonFatal
4649
4750final class EventSourcedStatefulService (val factory : EventSourcedEntityFactory ,
4851 override val descriptor : Descriptors .ServiceDescriptor ,
@@ -65,11 +68,53 @@ final class EventSourcedStatefulService(val factory: EventSourcedEntityFactory,
6568 this
6669}
6770
71+ object EventSourcedImpl {
72+ final case class EntityException (entityId : String , commandId : Long , commandName : String , message : String )
73+ extends RuntimeException (message)
74+
75+ object EntityException {
76+ def apply (message : String ): EntityException =
77+ EntityException (entityId = " " , commandId = 0 , commandName = " " , message)
78+
79+ def apply (command : Command , message : String ): EntityException =
80+ EntityException (command.entityId, command.id, command.name, message)
81+
82+ def apply (context : CommandContext , message : String ): EntityException =
83+ EntityException (context.entityId, context.commandId, context.commandName, message)
84+ }
85+
86+ object ProtocolException {
87+ def apply (message : String ): EntityException =
88+ EntityException (entityId = " " , commandId = 0 , commandName = " " , " Protocol error: " + message)
89+
90+ def apply (init : EventSourcedInit , message : String ): EntityException =
91+ EntityException (init.entityId, commandId = 0 , commandName = " " , " Protocol error: " + message)
92+
93+ def apply (command : Command , message : String ): EntityException =
94+ EntityException (command.entityId, command.id, command.name, " Protocol error: " + message)
95+ }
96+
97+ def failure (cause : Throwable ): Failure = cause match {
98+ case e : EntityException => Failure (e.commandId, e.message)
99+ case e => Failure (description = " Unexpected failure: " + e.getMessage)
100+ }
101+
102+ def failureMessage (cause : Throwable ): String = cause match {
103+ case EntityException (entityId, commandId, commandName, _) =>
104+ val commandDescription = if (commandId != 0 ) s " for command [ $commandName] " else " "
105+ val entityDescription = if (entityId.nonEmpty) s " entity [ $entityId] " else " entity"
106+ s " Terminating $entityDescription due to unexpected failure $commandDescription"
107+ case _ => " Terminating entity due to unexpected failure"
108+ }
109+ }
110+
68111final class EventSourcedImpl (_system : ActorSystem ,
69112 _services : Map [String , EventSourcedStatefulService ],
70113 rootContext : Context ,
71114 configuration : Configuration )
72115 extends EventSourced {
116+ import EventSourcedImpl ._
117+
73118 private final val system = _system
74119 private final val services = _services.iterator
75120 .map({
@@ -79,6 +124,8 @@ final class EventSourcedImpl(_system: ActorSystem,
79124 })
80125 .toMap
81126
127+ private val log = Logging (system.eventStream, this .getClass)
128+
82129 /**
83130 * The stream. One stream will be established per active entity.
84131 * Once established, the first message sent will be Init, which contains the entity ID, and,
@@ -99,18 +146,17 @@ final class EventSourcedImpl(_system: ActorSystem,
99146 case (Seq (EventSourcedStreamIn (InInit (init), _)), source) =>
100147 source.via(runEntity(init))
101148 case _ =>
102- // todo better error
103- throw new RuntimeException (" Expected Init message" )
149+ throw ProtocolException (" Expected Init message" )
104150 }
105151 .recover {
106- case e =>
107- // FIXME translate to failure message
108- throw e
152+ case error =>
153+ log.error(error, failureMessage(error))
154+ EventSourcedStreamOut ( OutFailure (failure(error)))
109155 }
110156
111157 private def runEntity (init : EventSourcedInit ): Flow [EventSourcedStreamIn , EventSourcedStreamOut , NotUsed ] = {
112158 val service =
113- services.getOrElse(init.serviceName, throw new RuntimeException ( s " Service not found: ${init.serviceName}" ))
159+ services.getOrElse(init.serviceName, throw ProtocolException (init, s " Service not found: ${init.serviceName}" ))
114160 val handler = service.factory.create(new EventSourcedContextImpl (init.entityId))
115161 val thisEntityId = init.entityId
116162
@@ -137,33 +183,35 @@ final class EventSourcedImpl(_system: ActorSystem,
137183 (event.sequence, None )
138184 case ((sequence, _), InCommand (command)) =>
139185 if (thisEntityId != command.entityId)
140- throw new IllegalStateException (" Receiving entity is not the intended recipient of command" )
141- val cmd = ScalaPbAny .toJavaProto(command.payload.get)
142- val context = new CommandContextImpl (thisEntityId,
143- sequence,
144- command.name,
145- command.id,
146- service.anySupport,
147- handler,
148- service.snapshotEvery)
186+ throw ProtocolException (command, " Receiving entity is not the intended recipient of command" )
187+ val cmd =
188+ ScalaPbAny .toJavaProto(command.payload.getOrElse(throw ProtocolException (command, " No command payload" )))
189+ val context =
190+ new CommandContextImpl (thisEntityId, sequence, command.name, command.id, service.anySupport, log)
149191
150192 val reply = try {
151- handler.handleCommand(cmd, context) // FIXME is this allowed to throw
193+ handler.handleCommand(cmd, context)
152194 } catch {
153- case FailInvoked =>
154- Optional .empty[ JavaPbAny ]()
155- // Ignore, error already captured
195+ case FailInvoked => Optional .empty[ JavaPbAny ]() // Ignore, error already captured
196+ case e : EntityException => throw e
197+ case NonFatal (error) => throw EntityException (command, " Unexpected failure: " + error.getMessage)
156198 } finally {
157199 context.deactivate() // Very important!
158200 }
159201
160202 val clientAction = context.createClientAction(reply, false )
161203
162204 if (! context.hasError) {
163- val endSequenceNumber = sequence + context.events.size
205+ // apply events from successful command to local entity state
206+ context.events.zipWithIndex.foreach {
207+ case (event, i) =>
208+ handler.handleEvent(ScalaPbAny .toJavaProto(event), new EventContextImpl (thisEntityId, sequence + i + 1 ))
209+ }
164210
211+ val endSequenceNumber = sequence + context.events.size
212+ val performSnapshot = (endSequenceNumber / service.snapshotEvery) > (sequence / service.snapshotEvery)
165213 val snapshot =
166- if (context. performSnapshot) {
214+ if (performSnapshot) {
167215 val s = handler.snapshot(new SnapshotContext with AbstractContext {
168216 override def entityId : String = entityId
169217 override def sequenceNumber : Long = endSequenceNumber
@@ -195,9 +243,9 @@ final class EventSourcedImpl(_system: ActorSystem,
195243 ))
196244 }
197245 case (_, InInit (i)) =>
198- throw new IllegalStateException ( " Entity already inited" )
246+ throw ProtocolException (init, " Entity already inited" )
199247 case (_, InEmpty ) =>
200- throw new IllegalStateException ( " Received empty/unknown message" )
248+ throw ProtocolException (init, " Received empty/unknown message" )
201249 }
202250 .collect {
203251 case (_, Some (message)) => EventSourcedStreamOut (message)
@@ -213,25 +261,22 @@ final class EventSourcedImpl(_system: ActorSystem,
213261 override val commandName : String ,
214262 override val commandId : Long ,
215263 val anySupport : AnySupport ,
216- val handler : EventSourcedEntityHandler ,
217- val snapshotEvery : Int )
264+ val log : LoggingAdapter )
218265 extends CommandContext
219266 with AbstractContext
220267 with AbstractClientActionContext
221268 with AbstractEffectContext
222269 with ActivatableContext {
223270
224271 final var events : Vector [ScalaPbAny ] = Vector .empty
225- final var performSnapshot : Boolean = false
226272
227273 override def emit (event : AnyRef ): Unit = {
228274 checkActive()
229- val encoded = anySupport.encodeScala(event)
230- val nextSequenceNumber = sequenceNumber + events.size + 1
231- handler.handleEvent(ScalaPbAny .toJavaProto(encoded), new EventContextImpl (entityId, nextSequenceNumber))
232- events :+= encoded
233- performSnapshot = (snapshotEvery > 0 ) && (performSnapshot || (nextSequenceNumber % snapshotEvery == 0 ))
275+ events :+= anySupport.encodeScala(event)
234276 }
277+
278+ override protected def logError (message : String ): Unit =
279+ log.error(" Fail invoked for command [{}] for entity [{}]: {}" , commandName, entityId, message)
235280 }
236281
237282 class EventSourcedContextImpl (override final val entityId : String ) extends EventSourcedContext with AbstractContext
0 commit comments