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

Producer initiated pushing of events over gRPC #932

Merged
merged 66 commits into from
Aug 15, 2023

Conversation

johanandren
Copy link
Member

@johanandren johanandren commented Jun 26, 2023

Superseedes #920

  • complete end user APIs Java and Scala
  • handling duplicates/redelivery in the event writer
  • more thorough test coverage for just the event writer
  • something like the producer interceptors for pluggable auth based on request metadata
  • some form of consumer-decided filtering, already on the producer
  • transformation of more than the persistence id on the consumer side, with access to origin id
  • initial docs

@johanandren johanandren force-pushed the wip-producer-event-push branch 3 times, most recently from 45e023f to 52d8b0c Compare June 27, 2023 14:20
oneof message {
// always sent first
ConsumerEventInit init = 1;
Event event = 2;
Copy link
Member

Choose a reason for hiding this comment

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

in event_producer.proto we have Event and FilteredEvent. Should we use FilteredEvent here too?

Copy link
Member Author

Choose a reason for hiding this comment

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

That would make the protocol more clear yeah, I'll do that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed in 80d67c0

}
.via(ActorFlow.askWithStatus(8)(eventWriter) {
(in: proto.Event, replyTo: ActorRef[StatusReply[EventWriter.WriteAck]]) =>
// FIXME would we want request metadata/producer ip/entire envelope in to persistence id transformer?
Copy link
Member

Choose a reason for hiding this comment

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

yes, one case could be that based on the origin of the event you want to adjust the persistenceId to include that origin (for example making it more unique)

Copy link
Member

Choose a reason for hiding this comment

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

another thing that could be good to amend before writing the event would be the tags, e.g. adding a tag for the origin, which can then be used in filtering later

Copy link
Member

Choose a reason for hiding this comment

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

we could have a transformation function of the Entire EventEnvelope, but we shouldn't allow changing the seqNr

btw, would then probably be nice with some "copy methods" on the EventEnvelope, such as withTags, withPersistenceId.

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess we could accept a transform but always check that sequence number was not changed. Or more a more limited transform API instead of adding API to let users actually transform the entire envelope, Transform.addTags().

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a try at a transform API but didn't include the origin as a parameter yet: fdb350e

Copy link
Member Author

Choose a reason for hiding this comment

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

And now the transform can be created per origin/event stream based on origin id and request metadata fda1176

// FIXME config for parallelism, and perPartition (aligned with event writer batch config)
.mapAsyncPartitioned(1000, 20)(_.persistenceId) { (in, _) =>
// FIXME would we want request metadata/producer ip/entire envelope in to persistence id transformer?
val envelope = ProtobufProtocolConversions.eventToEnvelope[Any](in, protoAnySerialization)
Copy link
Member

Choose a reason for hiding this comment

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

Would be good if we can skip the deserialization-serialization roundtrip here, and that may impact what kind of transformer we want to support. If the transformer is not changing the payload we can avoid the serialization.

Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

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

I just looked briefly at this again, let me know when it's time for a deeper look.


}
})
.onFailure[Exception](SupervisorStrategy.restart)
Copy link
Member

Choose a reason for hiding this comment

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

should have backoff restart, because if it starts failing we don't want to overload the db?

Copy link
Member Author

Choose a reason for hiding this comment

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

Honestly not quite sure how this would crash, other than some bug in the batching logic, errors persisting events are fed back to the sender and shouldn't crash the writer itself.

Copy link
Member

Choose a reason for hiding this comment

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

Right, we are not failing the writer actor when we there are db errors. The CircuitBreaker in AsyncWriteJournal will protect the db from overload when it's already in trouble. Ok, we can leave it as plain restart.

@johanandren
Copy link
Member Author

johanandren commented Jul 3, 2023

@patriknw the largest task that is left is to come up with proper user API, and once that is in place, break out the sample I started on to be able to test it properly, and some docs.

I think you can review the internal bits and pieces a bit more if you have time.

Possible small-to-medium things left, not sure if we need all right now or can iterate:

  • something like the producer interceptors for pluggable auth based on request metadata
  • still feels like some form of consumer-decided filtering would be good - filtering out events already on the producer, could maybe be static and sent as immediate response to the consumer opening up the stream
  • transformation of more than the persistence id on the consumer side
  • avoiding deserialization unless needed

@johanandren johanandren force-pushed the wip-producer-event-push branch 2 times, most recently from b426abe to 4f5e427 Compare July 4, 2023 11:51
@johanandren
Copy link
Member Author

@patriknw WDYT is this producer API simple enough while being flexible enough that you can run it in a non-clustered actor system to just push events for one or a few entities, but ofc also in a cluster with SDP to push a larger number of entities.

Or do we need something higher level like we did with the Replication which takes care of starting sharding, naming projections etc for the user?

}
}

case JournalProtocol.WriteMessageFailure(message, error, `actorInstanceId`) =>
Copy link
Member

Choose a reason for hiding this comment

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

How should we handle duplicates, which will result in write failures? We will have duplicates since it's at-least-once.

One idea is to optimistically write, but if it fails we can check if the pid/seqNr exists with EventTimestampQuery or LoadEventQuery, or even eventsByPersistenceId since we are interested in more than one in the batch.

Not great to add a dependency to readJournalPluginId too, so maybe we can do this with the journal protocol?
asyncReadHighestSequenceNr is not exposed as journal protocol message, so if we don't want to change Akka we could use ReplayMessages to find the highest sequence number.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, and the exception is from the plugin, so there is no way to determine that it is a duplicate-write from the failure response. Replay seems like a good trick, because we could do fromSequenceNr = toSequenceNr just for that seqNr.

Copy link
Member

Choose a reason for hiding this comment

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

Exception is from plugin, but we can't see the difference of duplicate error to any other error, at this level.
Some plugins may not fail on duplicates, such as Cassandra, but that is another story.

and I think you know which other seqNrs are pending from the same batch, and it's very likely that they will also fail. Then it's probably easier to use WriteMessagesFailure to trigger these checks. We know that all or none will fail because they are in AtomicWrite.

Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

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

EventWriter...

Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

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

javadsl looking good

@patriknw
Copy link
Member

patriknw commented Jul 7, 2023

avoiding deserialization unless needed

that can wait until later, probably a rather big task, if we think about all cases where it would be nice

@johanandren
Copy link
Member Author

This needs an Akka patch release with the event writer, but then I think we can merge and iterate on top.

@johanandren johanandren marked this pull request as ready for review July 10, 2023 13:40
Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

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

LGTM

.registerTagMapper[String](_ => Set("added-tag"))
.registerPayloadMapper[String, String](env => env.eventOption.map(_.toUpperCase))
}
.withConsumerFilters(Vector(ConsumerFilter.ExcludeEntityIds(Set(consumerFilterExcludedPid.id))))
Copy link
Member

Choose a reason for hiding this comment

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

looks good

@johanandren johanandren modified the milestones: 1.5.0-M1, 1.5.0-M2 Aug 15, 2023
@johanandren johanandren merged commit 3b8a054 into main Aug 15, 2023
@johanandren johanandren deleted the wip-producer-event-push branch August 15, 2023 16:17
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.

2 participants