aglassman
Problem
The cancel_async function is easily overlooked. Since the results of “outdated” tasks are ignored, it’s easy for developers to assume that the task was killed, even though it will continue to execute. The async_assign documentation does not make a reference to cancel_async, although start_async does.
I think this default is setting up users for issues down the road. Many times, these async functions are used to perform slow, or expensive database queries. Given that each task will checkout its own connection, this puts the LiveView at risk for putting undue pressure on the connection pool.
Suggestions
Suggestion 1
Add mention of cancel_async to the async_assign documentation.
Suggestion 2
assign_async and start_async should provide an opt called cancel, which would effectively do what cancel_async does.
usage: assign_async(socket, :records, fn → … end, cancel: true)
Suggestion 3
Provide a configuration to set the default for the cancel opt. This would allow a sensible default for the workload of the project, yet could still be overridden. This also provides an easy way to not break existing behavior for LiveView projects. The library default can be cancel_async_default: false, and teams can then determine if modifying this value makes sense. This minimizes the amount of code changes needed for an existing project, and removes the need for individuals on a team to remember to add cancel: ... to every usage of async_assign or start_async.
config :phoenix_live_view,
cancel_async_default: true
Trending in Proposals: Ideas
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #ai
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #elixirconf-eu
- #metaprogramming
- #hex










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
rhcarvalho
And perhaps even add to the overall Async Operations too: Phoenix.LiveView — Phoenix LiveView v1.2.5.
I imagine none of the maintainers would oppose to a PR doing just that!
I see a case where you’d want to possibly kill existing tasks started earlier, similar to what is described in the
start_async/4docs. So your intention is to make killing in-flight tasks the default, right?I think naming the option
:cancelis confusing, as reading the code in isolation makes me think of cancelling of the new task I’m about to start.Suggestion:
cancel_existing: trueorreplace: true.Idea: if you haven’t already, you can implement this in your project and report on the usage.
MyApp.Async.assign_async/4can replicate theassign_async/4signature, but additionally understand areplace: trueoption which makes it callcancel_async/3before delegating toassign_async/4.Now that I look at it, I see one thing this option would miss is the ability to pass a custom reason when cancelling the task.
I like that LiveView gives us the primitives. I’m not so sure that replacing in-flight tasks should be the default. If it was, and there was no
cancel_async/3, a reasonable ask would be how to keep in-flight tasks.garrison
For most simple tasks canceling by default is more effort than it’s worth. That is, for a simple async assign like “fetch a row from the DB” you’re unlikely to glean any additional performance killing the task a few milliseconds earlier than it would have completed. What’s important is that the newer task’s result clobbers the old one, and LiveView already does the right thing there.
For long-running tasks canceling makes more sense, but these are less common so I think the current default is quite reasonable.
Ask for the
canceloption (cancel_existingwould indeed be clearer), I’m not really sold either way.The only meaningful advantage here is that you don’t have to write the key twice. But I could see that being a slight improvement in some cases.
tfwright
I like this suggestion. Don’t know enough about the internal of the async assigns implementation to consider downsides, and I suspect there might be some if it is not already the current behavior. As a user it certainly seems cleaner for the system to take care of cancelling any ongoing processes related to the previous assign for me. I’ve done something similar most of the time when creating APIs that manage async processes–if the user makes a second request while the first is ongoing, cancel it before starting the new one.
derek-zhou
Function calls with a inline closure and a keyword list are ugly. I suggest to name the new behaviors, sans the option,
reassign_asyncandrestart_asyncand keep the old behaviors to the old functions.garrison
Lol actually I think the inline closure plus kwlist looks quite good, so I guess this is a case of aesthetic preference.
Wrapper functions are another option. You could even just do it yourself:
aglassman
Great suggestion, agree here.
Yes, I think I will try it out and report back.
I agree here. The benefit of killing the task as the default is mainly when DB usage is spiked / an unexpected issue is impacting query performance. The users starts clicking around the page, spawning many tasks that are just putting pressure on the connection pool. Or, a LiveView is subscribed to a PubSub topic, and the code broadcasting has a bug that starts emitting events at an elevated rate. (Both are real situations I’ve encountered, and using cancel_async fixed most of the issues).
Agreed, this is why I thought a global default (suggestion 3) would be an advantage. Make the default the “safest” option, while still giving the individual the option to override explicitly.
I wouldn’t be opposed to that option. Refactoring existing code to use this wouldn’t be too difficult, and it would be clear to developers that it manages tasks differently than async_assign / start_async.
rhcarvalho
One way to accomplish this for your whole app/team, with a similar effect than the config you suggested, is to replace
assign_asyncandstart_asyncin yourMyAppWebmodule.I wrote more details with a code example in the forum before, turns out it was also about
assign_async!tfwright
Based on how core teams in the ecosystem have responded to similar proposals, I think this is the most likely response. And although I grumble it makes sense. When Elixir makes it so easy to make such things ergonomic while keeping them explicit and minimizing API surface, probably best to prefer that.
derek-zhou
The problem with inline closure and a keyword list as arguments are they both prefer to be the last of the argument list. In the case of keyword list, you can skip the
[]and it sorta looks like named arguments. In the case of inline closures, they could be long and anything beyond the non-trivial closure will feel out-of-place.garrison
I guess I see what you mean. Honestly I don’t think it looks too bad. Maybe if the closure was really long, but at that point I’d probably split it up.
In this case the discussion is kinda moot, though, as the function already takes a kwlist.