-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
support handling multiple events #363
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please set your IDE to automatically convert tabs to spaces. I see a few tabs that should be spaces.
response.events.isEmpty match { | ||
case true => | ||
response.event match { | ||
case Some(newEvent) => Seq(newEvent) | ||
case None => Seq.empty[any.Any] | ||
} | ||
case _ => response.events | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
response.events.isEmpty match { | |
case true => | |
response.event match { | |
case Some(newEvent) => Seq(newEvent) | |
case None => Seq.empty[any.Any] | |
} | |
case _ => response.events | |
}) | |
if (response.events.isEmpty) { | |
response.event match { | |
case Some(newEvent) => Seq(newEvent) | |
case None => Seq.empty[any.Any] | |
} | |
} else { | |
response.events | |
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though I am kind of curious here, is i t possible for events and event to both be populated? If so, do we want to preserve both events and event?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference is that we use events unless it is not populated.
I stated it in the proto change https://github.com/namely/chief-of-state-protos/pull/23/files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would actually fail if both are set.
} | ||
case _ => response.events | ||
}) | ||
.map(_.filter(x => x != any.Any()) // ignore empty events and validate types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.map(_.filter(x => x != any.Any()) // ignore empty events and validate types | |
.map(_.filter(_ != any.Any.defaultInstance) // ignore empty Google Any |
.map(x => | ||
x match { | ||
case None => WriteHandlerHelpers.NoOp | ||
case Some(newState) => newState | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.map(x => | |
x match { | |
case None => WriteHandlerHelpers.NoOp | |
case Some(newState) => newState | |
}) | |
.map { | |
case None => WriteHandlerHelpers.NoOp | |
case Some(newState) => newState | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
put the parentheses tho
.flatMap({ | ||
case WriteHandlerHelpers.NewEvent(newEvent) => | ||
// state will be updated with the resulting state of each HandleEventResponse | ||
var state = priorState.getState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would remove this line as it is an anti-pattern in scala.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what would you suggest to do instead? what exactly is the anti pattern?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're mutating the "state" as you're running your code without a real reason to do so. In functional programming, you typically want to pass around immutable objects.
val newEventMeta: MetaData = MetaData() | ||
.withRevisionNumber(priorState.getMeta.revisionNumber + 1) | ||
.withRevisionDate(Instant.now().toTimestamp) | ||
.withData(data) | ||
.withEntityId(priorState.getMeta.entityId) | ||
.withHeaders(command.persistedHeaders) | ||
|
||
val priorStateAny: com.google.protobuf.any.Any = priorState.getState | ||
val priorStateAny: com.google.protobuf.any.Any = state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
val priorStateAny: com.google.protobuf.any.Any = state | |
val priorStateAny: com.google.protobuf.any.Any = priorState.getState |
state = response.getResultingState | ||
WriteHandlerHelpers.NewState(newEvent, state, newEventMeta) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
state = response.getResultingState | |
WriteHandlerHelpers.NewState(newEvent, state, newEventMeta) | |
WriteHandlerHelpers.NewState(newEvent, response.getResultingState, newEventMeta) |
|
||
}) | ||
// extract WriteTransitions of each Try or fail, then get the last one (if any exists) | ||
.map(_.get).lastOption) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.map(_.get).lastOption) | |
.map(_.get).lastOption | |
) | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wasn't that supposed to get auto formatted or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe there's an auto format option for where parenthesis end. I just figure it would be much easier to understand this snippet since the )
has nothing to do with that line.
case None => WriteHandlerHelpers.NoOp | ||
case Some(newState) => newState | ||
}) | ||
.recoverWith(makeFailedStatusPf) | ||
handlerOutput match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add a line break above.
Another thing I noticed is that you're calling .map multiple times. I don't believe there's a reason to not just combine your multiple .maps into one .map For instance, your handleRemoteCommand could be written like this
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing I noticed is that you're calling .map multiple times. I don't believe there's a reason to not just combine your multiple .maps into one .map
For instance, your handleRemoteCommand could be written like this
def handleRemoteCommand( priorState: StateWrapper, command: RemoteCommand, replyTo: ActorRef[CommandReply], commandHandler: RemoteCommandHandler, eventHandler: RemoteEventHandler, protosValidator: ProtosValidator, data: Map[String, com.google.protobuf.any.Any]): ReplyEffect[EventWrapper, StateWrapper] = { val handlerOutput: Try[WriteHandlerHelpers.WriteTransitions] = commandHandler .handleCommand(command, priorState) .map(response => { // use events or fallback to event in case events is empty val s: Seq[any.Any] = if (response.events.isEmpty) { response.event match { case Some(newEvent) => Seq(newEvent) case None => Seq.empty[any.Any] } } else { response.events } val filteredEvents: Seq[any.Any] = s .filter(_ != any.Any.defaultInstance) // ignore empty events and validate types .map(newEvent => { protosValidator.requireValidEvent(newEvent) newEvent }) // call handleEvent for each event, while passing the updated resulting state from previous call val writeTransaction: Option[NewState] = filteredEvents.map(newEvent => { val newEventMeta: MetaData = MetaData() .withRevisionNumber(priorState.getMeta.revisionNumber + 1) .withRevisionDate(Instant.now().toTimestamp) .withData(data) .withEntityId(priorState.getMeta.entityId) .withHeaders(command.persistedHeaders) eventHandler .handleEvent(newEvent, priorState.getState, newEventMeta) .map(response => { require(response.resultingState.isDefined, "event handler replied with empty state") protosValidator.requireValidState(response.getResultingState) WriteHandlerHelpers.NewState(newEvent, response.getResultingState, newEventMeta) }) }).map(_.get).lastOption //extract WriteTransitions of each Try or fail, then get the last one (if any exists) writeTransaction.getOrElse(WriteHandlerHelpers.NoOp) }) .recoverWith(makeFailedStatusPf) handlerOutput match { case Success(NoOp) => Effect.reply(replyTo)(CommandReply().withState(priorState)) case Success(NewState(event, newState, eventMeta)) => persistEventAndReply(event, newState, eventMeta, replyTo) case Failure(e: StatusException) => // record the exception on the current span Span.current().recordException(e).setStatus(StatusCode.ERROR) // reply with the error status Effect.reply(replyTo)(CommandReply().withError(toRpcStatus(e.getStatus, e.getTrailers))) case x => // this should never happen, but here for code completeness val errStatus = Status.INTERNAL.withDescription(s"write handler failure, ${x.getClass}") Span.current().recordException(errStatus.asException()).setStatus(StatusCode.ERROR) Effect.reply(replyTo)(CommandReply().withError(toRpcStatus(errStatus))) } }
but we still have to call map on the Seq twice, so what we are saving here is the map call on the Try?
Does that make a difference?
.flatMap({ | ||
case WriteHandlerHelpers.NewEvent(newEvent) => | ||
// state will be updated with the resulting state of each HandleEventResponse | ||
var state = priorState.getState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what would you suggest to do instead? what exactly is the anti pattern?
response.events.isEmpty match { | ||
case true => | ||
response.event match { | ||
case Some(newEvent) => Seq(newEvent) | ||
case None => Seq.empty[any.Any] | ||
} | ||
case _ => response.events | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference is that we use events unless it is not populated.
I stated it in the proto change https://github.com/namely/chief-of-state-protos/pull/23/files
|
||
}) | ||
// extract WriteTransitions of each Try or fail, then get the last one (if any exists) | ||
.map(_.get).lastOption) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wasn't that supposed to get auto formatted or something?
In my opinion, it is much more pleasant to read if you're not calling .map multiple times. For instance, this line is unpleasant and it is hard to tell where it ends (on line 223). I feel like your thought process by doing this is to physically segregate your transformations. If that's the case I would actually write out the mini functions where they're testable and plug them in directly into your map function. It allows for better unit tests. |
resolves #259:
Make sure to merge proto change pr before merging this PR.