Skip to content
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

Closed
wants to merge 7 commits into from

Conversation

ido-namely
Copy link
Contributor

@ido-namely ido-namely commented May 24, 2021

resolves #259:

  1. Call handleEvent for each event in events field.
  2. fallback to event if events is empty.

Make sure to merge proto change pr before merging this PR.

@codecov
Copy link

codecov bot commented May 24, 2021

Codecov Report

Merging #363 (dad125c) into master (ade4b21) will increase coverage by 1.65%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #363      +/-   ##
==========================================
+ Coverage   94.63%   96.28%   +1.65%     
==========================================
  Files          37       15      -22     
  Lines         745      404     -341     
  Branches       15        8       -7     
==========================================
- Hits          705      389     -316     
+ Misses         40       15      -25     
Impacted Files Coverage Δ
.../scala/com/namely/chiefofstate/AggregateRoot.scala 95.77% <100.00%> (+0.46%) ⬆️
...ely/chiefofstate/serialization/CosSerializer.scala
...m/namely/chiefofstate/ServiceMigrationRunner.scala
...ely/chiefofstate/readside/ReadSideProjection.scala
...la/com/namely/chiefofstate/config/BootConfig.scala
...m/namely/chiefofstate/config/WriteSideConfig.scala
.../com/namely/chiefofstate/config/EventsConfig.scala
...om/namely/chiefofstate/config/ReadSideConfig.scala
...namely/chiefofstate/readside/ReadSideHandler.scala
...y/chiefofstate/config/TelemetryConfigFactory.scala
... and 14 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ade4b21...dad125c. Read the comment docs.

Copy link
Contributor

@AntonioYuen AntonioYuen left a 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.

Comment on lines +189 to +196
response.events.isEmpty match {
case true =>
response.event match {
case Some(newEvent) => Seq(newEvent)
case None => Seq.empty[any.Any]
}
case _ => response.events
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
})

Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map(_.filter(x => x != any.Any()) // ignore empty events and validate types
.map(_.filter(_ != any.Any.defaultInstance) // ignore empty Google Any

Comment on lines +224 to +228
.map(x =>
x match {
case None => WriteHandlerHelpers.NoOp
case Some(newState) => newState
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map(x =>
x match {
case None => WriteHandlerHelpers.NoOp
case Some(newState) => newState
})
.map {
case None => WriteHandlerHelpers.NoOp
case Some(newState) => newState
}

Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val priorStateAny: com.google.protobuf.any.Any = state
val priorStateAny: com.google.protobuf.any.Any = priorState.getState

Comment on lines +218 to +219
state = response.getResultingState
WriteHandlerHelpers.NewState(newEvent, state, newEventMeta)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)
Copy link
Contributor

@AntonioYuen AntonioYuen May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map(_.get).lastOption)
.map(_.get).lastOption
)

Copy link
Contributor Author

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?

Copy link
Contributor

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 {
Copy link
Contributor

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.

@AntonioYuen
Copy link
Contributor

AntonioYuen commented May 24, 2021

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)))
    }
  }

Copy link
Contributor Author

@ido-namely ido-namely left a 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
Copy link
Contributor Author

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?

Comment on lines +189 to +196
response.events.isEmpty match {
case true =>
response.event match {
case Some(newEvent) => Seq(newEvent)
case None => Seq.empty[any.Any]
}
case _ => response.events
})
Copy link
Contributor Author

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)
Copy link
Contributor Author

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?

@AntonioYuen
Copy link
Contributor

AntonioYuen commented May 24, 2021

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?

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.

@klvmungai klvmungai closed this Oct 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Return multiple events from one command
5 participants