Honestly it looks overcomplicated for me and firstly I would try to refactor app to not fetch “half of database”. Not sure what you are trying to render, but in most cases you could just simplify your templates.
If you really need to load nested data concurrently you can write something like:
defmodule Example do
def sample(list) do
{children, list} = organise_nested(list)
list = [children | organise_flat(list)]
Enum.reduce(list, %{}, &load_many/2)
end
defp load_many(list, acc) do
list
|> Enum.map(&Task.async(fn -> {&1, load(&1, acc)} end))
|> Task.await_many()
|> Map.new()
|> Map.merge(acc)
end
defp load(:project, %{permissions: _permissions}) do
# …
end
defp load(:org, _acc) do
# …
end
defp load(:user, _acc) do
# …
end
defp load(:artifacts, _acc) do
# …
end
defp organise_flat(list) do
{left, right} = Enum.split_with(list, &is_atom/1)
if right == [] do
[left]
else
right =
Enum.map(right, fn {parent, children} ->
children = Enum.reject(children, &(&1 in left))
if children == [], do: parent, else: {parent, children}
end)
[left | organise_flat(right)]
end
end
defp organise_nested(list) do
{list, children} = organise_nested(list, [])
list = Enum.reverse(list)
children = children |> Enum.reverse() |> Enum.uniq()
{children, list}
end
defp organise_nested([], list), do: {[], list}
defp organise_nested([head | tail], list) when is_atom(head) do
result = organise_nested(tail, [head | list])
if is_list(result), do: {[], result}, else: result
end
defp organise_nested([{parent, children} | tail], list) do
children_list =
children
|> Enum.reduce([], fn
{child, _child_children}, acc -> [child | acc]
_child, acc -> acc
end)
|> Enum.reverse()
parent_result = if children_list == [], do: parent, else: {parent, children_list}
{nested_result, list} = organise_nested(children, list)
{tail_result, list} = organise_nested(tail, list)
{tail_result ++ [parent_result | nested_result], list}
end
end
With this you would have 4 calls to Task.wait_many/1
:
- Load
b
, d
and f
as they are leafs
- Load
e
as it requires f
- Load
c
as it requires e
- Load
a
as it requires c
The downside of this is that e
would wait for f
as well as b
and d
. However if you would rewrite this code to work on each nested level then you would have a similar problem as b
would then wait for d
, e
and f
when it does not requires anything.
If you want to even go further and fix that then you would need to replace this code:
list = [children | organise_flat(list)]
Enum.reduce(list, %{}, &load_many/2)
with a call to your custom function like:
# notice no organise_flat/1 call here
# children looks like: [:b, :d, :f]
# list looks like: [:e, {:c, [:e]}, {:a, [:c]}]
custom_await_many(children, list)
You would need to look at source of Task.await_many/1
and rewrite it so:
- First arguments
children
(leafs) are changed to tasks with Task.async/1
call and those would be added to awaiting
- When
receive
block handles reply
and like {:project, project}
then you need to call Map.put/3
for replies and add condition that if said reply was last needed then add to awaiting more things like:
{child_name, result} = reply
replies = Map.put(replies, child_name, result)
[unblocked, blocked] =
Enum.reduce(blocked, {[], []} fn
{name, [child_name]}, [unblocked, blocked] ->
[[name | unblocked], blocked]
{name, children}, [unblocked, blocked] ->
children = Enum.reject(children, &(&1 == name))
[unblocked, [{name, children} | blocked]
end)
# now change every unblocked to task using replies
# go back to `receive` block waiting for other replies
Enum.reject(children, & &1 == name)Enum.reject(children, & &1 == name)
In all cases it’s always the same. If load
function would return an error all you need to do is to stop working. As posted above simple condition like:
failed_replies = Enum.reject(replies, &(elem(&1, 0) == :ok)
if failed_replies == [] do
# …
else
# …
end
and rest would be done by pattern-matching - it’s as simple in reduce
function as well as when writing a custom await_many
.