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

native/subscription/unfold: terminate stream when user returns None #1553

Closed

Conversation

fengalin
Copy link

Context

While experimenting with iced for a WIP application, I ran into issues with the subscription::unfold behaviour. This MR proposes a change to the API which would solve my issues and which I believe would be preferable for iced in general.

Note: I read the contributing guidelines. Since this seemed like a small improvement to me, I didn't feel the need to introduce myself on the Discord server. In order to reflect on this I had to experiment to the extent of this MR anyway. Please tell me if I was wrong!

Analysis

futures::stream::unfold builds a Stream from the provided seed of type T and a Future returned by the f function. The resulting Stream produces values as long the Future returns Some tuple (Item, T). If the Future returns None, unfold is supposed to stop producing items.

In iced, the subscription::unfold function is based on the stream::unfold. However the Output of the Future is defined as (Option<Message>, T). The Future can return a None Message and the Stream will keep running.

The above behaviour is exemplified in examples/download_progress. When the download is finished (either because it is complete or if an error occurred), the Future is still being called. Adding a println statement shows that the Future is called repeatedly. The original code added a pending().await as a workaround. This means that the executor Tasks is kept in queues even if it will no longer make progress.

The first commit removes the pending().await workaround in download_progress and adds stdout & stderr traces so as to assess the impact of the second commit.

Proposal

One way to solve this would be to keep the existing signature for subscription::unfold and to map the Future output in such a way that it return None if the provided Option<Message> is None. However, this would be inconsistent with the unfold function from the futures crate and it wouldn't be suitable since the state is no longer usable after the Stream
completes.

This MR aligns the subscription::unfold function with its futures counterpart. In the download_progress example, after a download is finished, the Task in no longer kept in queues.

This changes a bit the implementation for the websocket example. Previous implementation returned from the Future with a None message and an unchanged state when the websocket or input were awaken but the result didn't need to be propagated. With this MR, in such cases the implementation loops on the wbesocket & input async select awaiting for the next message to be propagated.

The last commit is a small unrelated improvement to the download_progress example.

@n1ght-hunter
Copy link
Contributor

couldn't you just put a simple if statement that when your subscription is finished you no longer return it?

 if self.continue_websocket {
    echo::connect().map(Message::Echo)
} else {
    iced::subscription::Subscription::none()
}

@fengalin
Copy link
Author

fengalin commented Dec 9, 2022

Thanks for the suggestion @Night-Hunter-NF. I faced the issue addressed in this PR while already doing this.

@fengalin fengalin force-pushed the subscription-unfold-none-handling branch from f843652 to 043b691 Compare December 9, 2022 21:37
@hecrj hecrj added improvement An internal improvement shell labels Dec 20, 2022
@hecrj hecrj added this to the 0.7.0 milestone Dec 20, 2022
@fengalin fengalin force-pushed the subscription-unfold-none-handling branch from 043b691 to 3ab18d2 Compare December 23, 2022 09:11
@fengalin fengalin force-pushed the subscription-unfold-none-handling branch from 3ab18d2 to 0246fc1 Compare January 8, 2023 15:54
@hecrj hecrj modified the milestones: 0.7.0, 0.8.0 Jan 14, 2023
This commit shows how the subscription `Future` is called repeatedly
after the `Finished` state is reached, even after returning a `None`
message. This is the reason for the `pending().await` workaround in
the original code.

See next commit for a fix which allows returning `None` to stop the
`Stream`, similarly to how `futures::stream::unfold` behaves.
`futures::stream::unfold` builds a `Stream` from the provided seed
of type `T` and a `Future` returned by the `f` function. The resulting
`Stream` produces values as long the `Future` returns `Some` tuple
`(Item, T)`. If the `Future` returns `None`, `unfold` is supposed to
stop producing items.

In `iced`, the `subscription::unfold` function is based on the
`stream::unfold`. However the `Output` of the `Future` is defined as
`(Option<Message>, T)`. The `Future` can return a `None` `Message`
and the `Stream` will keep running.

The above behaviour is exemplified in `examples/download_progress`.
When the download is finished (either because it is complete or
if an error occurred), the `Future` is still being called. Adding
a `println` statement shows that the `Future` is called repeatedly.
The original code added a `pending().await` as a workaround. This
means that the executor `Tasks` is kept in queues even if it will
no longer make progress.

One way to solve this would be to keep the existing signature for
`subscription::unfold` and to `map` the `Future` output in such a
way that it return `None` if the provided `Option<Message>` is
`None`. However, this would be inconsistent with the `unfold`
function from the `futures` crate and it wouldn't be suitable
since the `state` is no longer usable after the `Stream`
completes.

This commit aligns the `subscription::unfold` function with its
`futures` counterpart. In the `download_progress` example, after
a download is finished, the `Task` in no longer kept in queues.

This changes a bit the implementation for the `websocket` example.
Previous implementation returned from the `Future` with a `None`
message and an unchanged state when the websocket or input are
awaken but the result doesn't need to be propagated. With this
commit, in such cases the implementation loops on the wbesocket &
input async select awaiting for the next message to be propagated.
The `on_press` handler was not defined for the 'Start again' button,
so the button was inactive after a download completion.
@fengalin fengalin force-pushed the subscription-unfold-none-handling branch from 0246fc1 to 6ecb44e Compare February 1, 2023 14:10
@hecrj hecrj modified the milestones: 0.8.0, 0.9.0 Feb 18, 2023
@hecrj
Copy link
Member

hecrj commented Apr 11, 2023

While the changes at first glance seem reasonable and consistent, the main issue here is that a Subscription should not be allowed to terminate. The Application is the only one capable of dictating which subscriptions are born and terminated.

Therefore, exposing an API that seems to indicate subscriptions can "end" by themselves seems misleading. It is true that we are already exposing Stream in Recipe and you can definitely return a Stream that terminates there; but that API is considered relatively internal (or "advanced").

@hecrj
Copy link
Member

hecrj commented Apr 11, 2023

This MR proposes a change to the API which would solve my issues

I'd like to know more about these issues. Is there something you need to do that the current unfold helper doesn't allow?

@hecrj
Copy link
Member

hecrj commented Apr 11, 2023

I believe the main issue here is that unfold should not have an Option at all to begin with. This makes it seem that returning None actually terminates the Subscription when all it does is loop.

I have made that change in #1786 and introduced a new helper channel that should simplify state management a bunch when compared to unfold.

@hecrj hecrj closed this in #1786 Apr 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
improvement An internal improvement shell
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants