Does LiveView's assign_async accept the timeout option?

Hey there,
While trying to temporarily fix a laggy UI (with long DB operations) I am left wondering if assign_async accepts e.g. timeout: :timer.seconds(30) at all?

I am looking at the source of Phoenix.LiveView.Async and it does not seem to use opts for anything outside supervisor and reset, as per its docs. Still, the code uses macro environments and I did not scan the entire code base of LV so I might be missing an implicit context.

Does anybody know? And, assuming the answer is “no”, what options do I have to give assign_async a bigger breathing room for executing its enclosed function?

Are you sure it’s a timeout with async you’re running in? Afaik async callbacks simply run for as long as they run – no timeout involved.

Fairly sure. The function enclosed in assign_async is doing some very slow DB work and then I get a stacktrace right in the UI that clearly blames Phoenix.LiveView.Async.assign_async almost near the bottom. The bottom-most entry is Task.Supervised.invoke_mfa.

Here’s what I get in the UI:

<other_functions>,
{Phoenix.LiveView.Async, :"-assign_async/4-fun-1-", 2, [file: ~c"lib/phoenix_live_view/async.ex", line: 141]},
{Phoenix.LiveView.Async, :do_async, 5, [file: ~c"lib/phoenix_live_view/async.ex", line: 213]},
{Task.Supervised, :invoke_mfa, 2, [file: ~c"lib/task/supervised.ex", line: 101]}]}}

Actually wait, I might have misunderstood you. Task.start_link – which the code ends up using because it does not supply the :supervisor option to assign_async – indeed does not impose a timeout. Must be something higher up, like Ash (which the project uses). Oh well, now I am mystified.

Sorry for the ping @zachdaniel. We’re using Ash.read! inside the body of the function that’s nested in LiveView’s assign_async. We are giving a very generous timeout (minutes) as an argument to Ash.read!, yet Ash – or something else, it’s not yet clear enough – is cancelling the queries long before that – it seems to be 15 seconds by my own manual mental counting but I wouldn’t bet my life on it.

Here’s a redacted code:

    resource
    |> Ash.Query.new()
    |> filter_by_stuff_id(stuff_id)
    |> Ash.read!(timeout: :timer.minutes(5))

This is a body of a function that’s executed once inside the function nested inside assign_async. And assign_async is itself called N times with each resource.

Here’s a word soup with our app redacted, in case it helps:

Error: {:exit, {%Ash.Error.Unknown{bread_crumbs: [“Error returned from: OURAPP.Stuff.Stuff.Issue.Version.read”], query: “#Query<>”, errors: [%Ash.Error.Unknown.UnknownError{error: “** (Postgrex.Error) ERROR 57014 (query_canceled) canceling statement due to user request”, field: nil, value: nil, splode: Ash.Error, bread_crumbs: [“Error returned from: OURAPP.Stuff.Stuff.Issue.Version.read”], vars: , path: , stacktrace: splode.Stacktrace<>, class: :unknown}]}, [{Ash.Error.Unknown, :“exception (overridable 2)”, 1, [file: ~c"lib/ash/error/unknown.ex", line: 3]}, {Ash.Error, :to_class, 2, [file: ~c"/app/deps/splode/lib/splode.ex", line: 264]}, {Ash.Error, :to_error_class, 2, [file: ~c"lib/ash/error/error.ex", line: 108]}, {Ash.Actions.Read, :do_run, 3, [file: ~c"lib/ash/actions/read/read.ex", line: 402]}, {Ash.Actions.Read, :“-run/3-fun-7-”, 3, [file: ~c"lib/ash/actions/read/read.ex", line: 82]}, {Ash.Actions.Read, :run, 3, [file: ~c"lib/ash/actions/read/read.ex", line: 81]}, {Ash, :read, 2, [file: ~c"lib/ash.ex", line: 2044]}, {Ash, :read!, 2, [file: ~c"lib/ash.ex", line: 2002]}, {OURAPP_Web.Live.AdminPanel.EventLogLive, :“-load_each_source_async/1-fun-1-”, 2, [file: ~c"lib/OURAPP_WEB/live/admin_panel/event_log_live.ex", line: 115]}, {Phoenix.LiveView.Async, :“-assign_async/4-fun-1-”, 2, [file: ~c"lib/phoenix_live_view/async.ex", line: 141]}, {Phoenix.LiveView.Async, :do_async, 5, [file: ~c"lib/phoenix_live_view/async.ex", line: 213]}, {Task.Supervised, :invoke_mfa, 2, [file: ~c"lib/task/supervised.ex", line: 101]}]}}

Ash’s version is 3.4.67. Recent enough I’d think.

Do you have any hints?

1 Like

:thinking: One thing to try (it shouldn’t matter) is to do Ash.Query.set_timeout(query, timeout).

Another thing to try is upping your repo timeout in config, that could be the issue. If so, we may actually be missing something which is passing the timeout down into the call to reads that are not in transactions :thinking:

1 Like

Something you could try is setting transaction? true on the read action, as that I know changes the way timeouts are handled. Would maybe point us in the right direction.

1 Like

Since it’s nearly impossible to replicate prod environment locally, we’ll have to try all of those at once in a single deployment and also add more metrics or OTel spans (or both)… and maybe even extra logging.

Thanks. Promise to come back here and let you know.

Yeah, the problem with this is that we are doing a for loop (basically) and doing many assign_async actions and each does Ash.read! – think it’s going to be problematic to modify all of them with the transaction flag.

I already added Ash.Query.timeout – there’s no set_timeout btw, let me know if I am missing something – and waiting for prod deployment.