Skip to content
This repository was archived by the owner on Mar 16, 2022. It is now read-only.

Commit c394d25

Browse files
committed
Update failure handling in java support
1 parent 995778d commit c394d25

File tree

6 files changed

+449
-36
lines changed

6 files changed

+449
-36
lines changed

java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ object CloudStateRunner {
6262
* CloudStateRunner can be seen as a low-level API for cases where [[io.cloudstate.javasupport.CloudState.start()]] isn't enough.
6363
*/
6464
final class CloudStateRunner private[this] (_system: ActorSystem, services: Map[String, StatefulService]) {
65-
private[this] implicit final val system = _system
65+
private[javasupport] implicit final val system = _system
6666
private[this] implicit final val materializer: Materializer = ActorMaterializer()
6767

6868
private[this] final val configuration =

java-support/src/main/scala/io/cloudstate/javasupport/impl/Contexts.scala

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ private[impl] trait AbstractClientActionContext extends ClientActionContext {
6161
checkActive()
6262
if (error.isEmpty) {
6363
error = Some(errorMessage)
64+
logError(errorMessage)
6465
throw FailInvoked
6566
} else throw new IllegalStateException("fail(…) already previously invoked!")
6667
}
@@ -81,6 +82,8 @@ private[impl] trait AbstractClientActionContext extends ClientActionContext {
8182

8283
final def hasError: Boolean = error.isDefined
8384

85+
protected def logError(message: String): Unit = ()
86+
8487
final def createClientAction(reply: Optional[JavaPbAny], allowNoReply: Boolean): Option[ClientAction] =
8588
error match {
8689
case Some(msg) => Some(ClientAction(ClientAction.Action.Failure(Failure(commandId, msg))))

java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/AnnotationBasedEventSourcedSupport.scala

+5-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEnt
2525

2626
import scala.collection.concurrent.TrieMap
2727
import com.google.protobuf.{Descriptors, Any => JavaPbAny}
28+
import io.cloudstate.javasupport.impl.eventsourced.EventSourcedImpl.EntityException
2829
import io.cloudstate.javasupport.{EntityFactory, ServiceCallFactory}
2930

3031
/**
@@ -78,7 +79,7 @@ private[impl] class AnnotationBasedEventSourcedSupport(
7879
}
7980
handler.invoke(entity, event, ctx)
8081
case None =>
81-
throw new RuntimeException(
82+
throw EntityException(
8283
s"No event handler found for event ${event.getClass} on $behaviorsString"
8384
)
8485
}
@@ -88,7 +89,8 @@ private[impl] class AnnotationBasedEventSourcedSupport(
8889
behavior.commandHandlers.get(context.commandName()).map { handler =>
8990
handler.invoke(entity, command, context)
9091
} getOrElse {
91-
throw new RuntimeException(
92+
throw EntityException(
93+
context,
9294
s"No command handler found for command [${context.commandName()}] on $behaviorsString"
9395
)
9496
}
@@ -104,7 +106,7 @@ private[impl] class AnnotationBasedEventSourcedSupport(
104106
}
105107
handler.invoke(entity, snapshot, ctx)
106108
case None =>
107-
throw new RuntimeException(
109+
throw EntityException(
108110
s"No snapshot handler found for snapshot ${snapshot.getClass} on $behaviorsString"
109111
)
110112
}

java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala

+77-32
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import java.util.Optional
2020

2121
import akka.NotUsed
2222
import akka.actor.ActorSystem
23+
import akka.event.{Logging, LoggingAdapter}
2324
import akka.stream.scaladsl.Flow
2425
import com.google.protobuf.{Descriptors, Any => JavaPbAny}
2526
import 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}
3840
import 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}
4547
import io.cloudstate.protocol.event_sourced._
48+
import scala.util.control.NonFatal
4649

4750
final 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+
68111
final 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

Comments
 (0)