-
Notifications
You must be signed in to change notification settings - Fork 141
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
Graceful shutdown of a stream for a single subscription #1201
base: master
Are you sure you want to change the base?
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.
I didn't look at the implementation yet, only docs and tests.
zio-kafka-test/src/test/scala/zio/kafka/consumer/ConsumerSpec.scala
Outdated
Show resolved
Hide resolved
zio-kafka-test/src/test/scala/zio/kafka/consumer/ConsumerSpec.scala
Outdated
Show resolved
Hide resolved
zio-kafka-test/src/test/scala/zio/kafka/consumer/ConsumerSpec.scala
Outdated
Show resolved
Hide resolved
zio-kafka-test/src/test/scala/zio/kafka/consumer/ConsumerSpec.scala
Outdated
Show resolved
Hide resolved
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.
Still need more time to digest this.
zio-kafka/src/main/scala/zio/kafka/consumer/SubscriptionStreamControl.scala
Outdated
Show resolved
Hide resolved
zio-kafka/src/main/scala/zio/kafka/consumer/SubscriptionStreamControl.scala
Outdated
Show resolved
Hide resolved
Hmm, should we instead of this: Consumer.runWithGracefulShutdown(Consumer.partitionedStreamWithControl(Subscription.topics("topic150"), Serde.string, Serde.string)) {
stream => ...
} offer this: Consumer.partitionedStreamWithGracefulShutdown(Subscription.topics("topic150"), Serde.string, Serde.string) {
(stream, _) => stream.flatMapPar(...)
} The second parameter would be the |
If I understand it correctly, the proposal allows for more use cases; with it you can also call |
Well, I mean compared to just the
|
If resume after |
Well, in both proposals you can call I don't think you want to do anything after stop, but it would give you more explicit control when to stop, instead of when the scope ends. We probably need to decide if we want to add pause/resume in the future. If we do, we should add the |
Hey :) Thanks for the great work! Here's some initial feedback: I'm not a big fan of the To me, functions/methods returning it should return a Tuple SubscriptionStreamControl[Stream[Throwable, Chunk[(TopicPartition, ZStream[R, Throwable, CommittableRecord[K, V]])]]] in favor of: (Stream[Throwable, Chunk[(TopicPartition, ZStream[R, Throwable, CommittableRecord[K, V]])], SubscriptionStreamControl) Made the change in a PR to show/study how, to me, it simplifies things: https://github.com/zio/zio-kafka/pull/1207/files |
zio-kafka/src/main/scala/zio/kafka/consumer/internal/RunloopAccess.scala
Outdated
Show resolved
Hide resolved
zio-kafka/src/main/scala/zio/kafka/consumer/internal/RunloopAccess.scala
Outdated
Show resolved
Hide resolved
Didn't finish my review yet. I still have some parts of the code to explore/understand, but I have to go. I'll finish it later 🙂 |
Thanks for the feedback Jules. Agreed about the extra concept that would be unwanted. Check out my latest interface proposal where there is only a |
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.
Still reading the code...
zio-kafka/src/main/scala/zio/kafka/consumer/SubscriptionStreamControl.scala
Outdated
Show resolved
Hide resolved
zio-kafka/src/main/scala/zio/kafka/consumer/SubscriptionStreamControl.scala
Outdated
Show resolved
Hide resolved
zio-kafka/src/main/scala/zio/kafka/consumer/internal/Runloop.scala
Outdated
Show resolved
Hide resolved
I understand now that when graceful shutdown starts we're ending the subscribed streams. That should work nicely. Lets work out what will happen next to the runloop. The runloop would still be happily fetching records for that stream. When those are offered to the stream, We can do slightly better though. We're fetching and storing all these records in the queue for nothing, even potentially causing an OOM for systems that are tuned for the case where processing happens almost immediately. My proposal is to:
If you want, I can extend this PR with that proposal (or create a separate PR). |
@erikvanoosten If you have some time to implement those two things, by all means. |
@svroonland Done in commit 1218204. Now I am wondering, how can we test this? |
Change looks good. Totally forgot to implement this part. |
I was preparing some work for removing We've discussed it earlier (about a year ago), but perhaps this alternative API is more powerful for our users, for usage scenario's we haven't thought of.. #1501 |
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.
The Scope provided to the returned effect controls the lifetime of the subscription. The subscription is unsubscribed when the scope is ended. Calling [[StreamControl.end]] stops fetching data for the subscription partitions but will not unsubscribe until the Scope is ended.
How do you coordinate calling end
and closing the scope? Does end
return immediately, or does it block until its safe to close the scope? Or is there another way to know the scope may be closed?
When the scope is closed without Does that make sense? |
Yes, that makes sense. However, could we also wait until the stream provided by the user ends? |
The intended usage pattern of this construct is something like:
So no additional need to wait for the stream to end when closing the scope. In fact, if we do that we might actually limit useability in some use cases we haven't thought of yet. If you use |
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.
Nice! I'll go through it once more from top to bottom. Perhaps this evening.
zio-kafka/src/main/scala/zio/kafka/consumer/internal/PartitionStreamControl.scala
Outdated
Show resolved
Hide resolved
zio-kafka/src/main/scala/zio/kafka/consumer/internal/PartitionStreamControl.scala
Show resolved
Hide resolved
BTW, is the PR description up to date? |
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.
Unfortunately, I thought of a new problem. When there is a rebalance, new partitions might be assigned to this consumer, even though the consumer is trying to do a graceful shutdown! We need to make sure NOT to start new streams for subscriptions for which we received a EndStreamsBySubscription.
|
||
Use the `*StreamWithControl` variants of `plainStream`, `partitionedStream` and `partitionedAssignmentStream` for this purpose. These methods return a `StreamControl` object allowing you to stop fetching records and terminate the execution of the stream gracefully. | ||
|
||
There is also the `Consumer.runWithGracefulShutdown` method which can gracefully terminate the stream upon fiber interruption, useful for a controlled shutdown when your application is terminated. |
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.
Consumer.runWithGracefulShutdown
could use its own section (with example) in this document. IMHO it should go before this section so that programmer who are in a hurry find it quickly.
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.
Agreed, added to docs
zio-kafka/src/main/scala/zio/kafka/consumer/internal/RunloopAccess.scala
Outdated
Show resolved
Hide resolved
zio-kafka/src/main/scala/zio/kafka/consumer/internal/RunloopAccess.scala
Show resolved
Hide resolved
_ <- ZIO.foreachDiscard( | ||
state.assignedStreams.filter(stream => Subscription.subscriptionMatches(subscription, stream.tp)) | ||
)(_.end) |
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.
_ <- ZIO.foreachDiscard( | |
state.assignedStreams.filter(stream => Subscription.subscriptionMatches(subscription, stream.tp)) | |
)(_.end) | |
_ <- ZIO.foreachDiscard { | |
state.assignedStreams.filter(stream => Subscription.subscriptionMatches(subscription, stream.tp)) | |
}(_.end) |
Okay, my review is complete. Finally 🙂 |
Ouch, we'll need to think about how to implement and test this.. |
I can't think of anything else than adding terminating subscriptions to the runloop state. They can be removed as soon as the subscription is actually terminated. |
Implements functionality for gracefully stopping a stream for a single subscription: stop fetching records for the assigned topic-partitions but keep being subscribed so that offsets can still be committed. Intended to replace
stopConsumption
, which did not support multiple-subscription use cases.A new command
EndStreamsBySubscription
is introduced, which calls theend
method on thePartitionStreamControl
of streams matching a subscription. In the methodConsumer#runWithGracefulShutdown
we then wait for the user's stream to complete, before removing the subscription.This is experimental functionality, intended to replace
stopConsumption
at some point. Methods with this new functionality are offered besides existing methods to maintain compatibility.All the fiber and scope trickery proved to be very hard to get right (the lifetime of this PR is a testimony to that), and there may still be subtle issues here.This is now traced back to issue zio/zio#9288Implements some of #941.