shiroyasha
One common pattern I encounter in the codebases that our team is maintaining is loading several resources concurrently. Often these resources depend on each other in a way that you need to load resource A before you can load resource B.
Here is a toy example where one would load information about a user, an organization, a project, and a list of permissions. To write this, one could structure the code like in the following example:
with(
load_org_task <- Task.async(fn -> load_org(org_id) end),
load_user_task <- Task.async(fn -> load_user(user_id) end),
load_project_task <- Task.async(fn -> load_project(user_id) end),
{:ok, org} <- Task.await(load_org_task),
{:ok, user} <- Task.await(load_user_task),
{:ok, project} <- Task.await(load_project_task),
load_artifact_list_task <- Task.async(fn -> list_arifacts(org_id) end),
load_permissions_task <- Task.async(fn -> load_permissions(project) end),
{:ok, artifact_list} <- Task.await(load_artifact_list_task),
{:ok, permissions} <- Task.await(load_permissions)
) do
render(org, user, project, artifact_list, permissions)
else
{:error, :not_found} -> ...
end
While the above example works, I feel it is not elegant enough. If you add timeouts, logs, metrics, and error handling to the above, the code can become long and, if you are not careful, a bit confusing.
I’m investigating approaches to how we could make this pattern streamlined and a bit more elegant.
I have some ideas, for example, the one in this PR: [draft] Loader by shiroyasha · Pull Request #23 · renderedtext/elixir-util · GitHub, but I’m also curious how other teams approach this or if the idea that I presented in the PR has merit and solves a real problem.
Given the above pattern, what would be your approach to cleaning it up and making it tighter and nicer?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #phoenix_html
- #iex
- #elixirconf-us
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 6- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
Eiji
This looks much cleaner for me:
If only one tasks (
load_permissions) requires other task (load_project) then we do not loss anything if we merge them into one.With above we can store all functions in list and even map them to get a list of tasks. Once that’s done all we need to do is to await for all tasks in list and we can do it in just one function call.
I have added a simple one line check for all list elements if first element of tuple is
:ok, but you can use pattern-matching inwithas well.As far as I know
Task.await_many/1is concurrent, so it’s much better than fewTask.await/1calls separately.derpycoder
Hey @shiroyasha,
Checkout:
https://github.com/Nebo15/sage
Here are some arguments to use it:
Features
Goals of the Sage project:
Problematic code:
Sage’s Solution
Result
Along with a readable code, you are getting:
P.S. I am not super experienced with Elixir, but this library seemed useful so I kept it in my bookmarks.
shiroyasha
@Eiji @derpycoder, thank you guys for the response. Both options are a good improvement over the original code snippet.
For @Eiji solution, the things I’m not sure how to approach are
1 —deeper nested dag dependencies. For example:
2 — error handling, or to know at the end of the operation which resource failed to load and what the reason was.
For @derpycoder, nice example! I feel it is a bit more suited for resource creation than resource loading, but the patterns in the code can be reused and streamlined for loading.
Eiji
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:
With this you would have 4 calls to
Task.wait_many/1:b,dandfas they are leafseas it requiresfcas it requireseaas it requirescThe downside of this is that
ewould wait forfas well asbandd. However if you would rewrite this code to work on each nested level then you would have a similar problem asbwould then wait ford,eandfwhen it does not requires anything.If you want to even go further and fix that then you would need to replace this code:
with a call to your custom function like:
You would need to look at source of
Task.await_many/1and rewrite it so:children(leafs) are changed to tasks withTask.async/1call and those would be added to awaitingreceiveblock handlesreplyand like{:project, project}then you need to callMap.put/3for replies and add condition that if said reply was last needed then add to awaiting more things like:Enum.reject(children, & &1 == name)Enum.reject(children, & &1 == name)
In all cases it’s always the same. If
loadfunction would return an error all you need to do is to stop working. As posted above simple condition like:and rest would be done by pattern-matching - it’s as simple in
reducefunction as well as when writing a customawait_many.wanton7
Loading things concurrent isn’t always as good as it sounds. It’s possible it makes things lot worse. Just calling them sequentially and let different request basically do concurrent request to the database might be better because it’s more fair sharing of database resource between requests. We did this in the past and had to refactor our app away from it because one request would start lot of multiple data calls to database and block short request with only few database call that arrived little bit later. So I would think hard before doing this kind of “optimization”.
shiroyasha
@Eiji Thanks once again for the great breakdown. The organize_nested is definitely hitting on many things I’m looking for. I’ll try to extract the essence and generalize the snippet that you shared.
@wanton7 good reminder, and thank you for sharing your path of doing this and then refactoring it to a fully sequential approach. I hit this same concern in ~ 2020 when we introduced a lot more parallelism into our codebase to reduce the processing time. I have strong signals from the last three years (metrics & traces) that show that parallelism significantly improves overall performance.
How can this be? We are still hitting the same database, right?
It seems that two factors make this assumption incorrect.
First, most resources are heavily cached, with level 1 being the local in-memory cache, level 2 being cluster distributed cache or dedicated cache storage like Redis, and level 3 being the database. So a typical request would hit multiple independent data systems.
Secondly, not all of our requests are database bound. We are a CI/CD provider where internal and external resources like virtual machines, docker registries, blob storage, and job processing units provide their internal APIs.
Sequential vs. Fully Concurrent
These are the two extremes. Loading things one-by-one or spawning up concurrent tasks without limits. In my experience, both approaches are suboptimal. The first one doesn’t take advantage of available resources when they are readily available, while the second can introduce more harm than good, like in the example you shared.
Ideally, I would like to express the dependencies and let the system decide when it is best to go in parallel and when it is best to go one-by-one. Something like (pseudocode):
Once again, thank you all for sharing your input. It helps me a lot to clarify my thoughts and question my assumptions. I’ll make sure to share which direction my team will take and, of course, share some open-source code and examples.