ahferroin7

ahferroin7

Most efficient way for a process to voluntarily yield scheduling priority?

Background

I’m working on a Discord bot in Elixir that runs as two separate applications, the bot itself, and all the backend logic and data handling that needs to be done on behalf of the bot. The back end application has to do a lot of data processing on startup which it’s currently handling asynchronously (curretly, each component in the backend is a GenServer, and the init callback just immediately returns a continue instruction that triggers the actual data processing needed for initialization). This is working rather well overall, except for one specific component which has a long and computationally intensive initialization sequence after a change in either the code or the initialization data for that component.

The component in question needs to process a very large list (roughly 2900 items) by computing an SQL transaction for each item and then running that against a database. The amount of processing here is time-prohibitive if it needs to be done serially (each item in the list takes about 50-100ms to process and then run the SQL transaction, so the full list takes almost 5 minutes if run one-by-one), but there are a handful of computations that can be shared across all the items, so my current code is using Stream.chunk_every/1 and Task.async_stream/3 to run the initialization in a number of parallel chunks equal to the number of online schedulers like so:

items
|> Stream.chunk_every(div(length(items), System.schedulers_online()) + 1)
|> Task.async_stream(&process_chunk/1, ordered: false)
|> Enum.to_list()

This is working in terms of actually processing things correctly and making the initialization fast enough to be useful, but causing a completely different issue in that it’s blocking scheduling of other processes for a long time, which is causing the bot itself to fail initialization because it can’t finish starting up before this starts running.

The question

My first instinct here based on experience elsewhere is to have the process_chunk/1 function voluntarily yield scheduling priority (I suppose this translates to voluntarily moving to the end of the run-queue for the scheduler in BEAM terms) before it processes each individual item. Right now, I’m doing this by running :timer.sleep(1) at the beginning of each iteration within process_chunk/1, which seems to be working to ensure that other things can run but feels like a bit of a hack TBH and also adds to the overall initialization time for this component (it’s only ~91ms of extra time on my development box, but translates to ~734ms on the production system it will be running on due to a much lower scheduler count).

Is there some more efficient way to voluntarily yield scheduling priority in Elixir or Erlang? Or is there perhaps some other approach I could take here that still lets other things run without significantly impacting the initialization times for the component in question?

Most Liked

sasajuric

sasajuric

Author of Elixir In Action

This is a curious problem :slight_smile:

First, I’ll echo the sentiment of others that GenServer shouldn’t be blocking for a long time, because that might cause the rest of the system to block. This can be handled in a couple of ways:

  1. Process chunks synchronously during the app or server boot (e.g. in the init callback).

  2. Start a separate task which will start the async_stream, await for the results, and then do something with them (e.g. send them to other processes).

  3. Instead of waiting for all the tasks to finish in GenServer, handle task results as they arrive in handle_info.

However, given you description I’m not sure that this would solve the issue. It’s interesting that including :timer.sleep(1) in process_chunk removes the problem. This could indeed mean that schedulers are blocked, or alternatively that some locking takes place at the SQL level.

If the schedulers are blocked, a likely reason would be a custom native code (NIF). As mentioned by others, the scheduler does frequent preemptive context switching. Due to the functional nature of BEAM languages, functions are frequently invoked, while a single longer-running BIF will bump the reduction count by more than 1. Furthermore, in recent OTP versions BIFs also yield. E.g. since OTP 22, length/1 will yield when called with long lists (source).

To check this I’d try to reproduce the problem using a single scheduler thread. I’d write a test function, e.g. process_big_chunk/0 which processes a larger amount of data sequentially (i.e. no tasks). I’d also comment out the startup processing code, i.e. I’d make the app start the required processes without doing anything else (like starting some activity).

Then I’d manually start a single-scheduler-threaded BEAM with ELIXIR_ERL_OPTIONS="+S 1" iex -S mix. From the iex session I’d first start the oberver (:observer.start), and then spawn an infinite processing loop as:


spawn(fn -> Stream.repeatedly(&process_big_chunk/0) |> Stream.run() end)`

If the observer is responsive (you can click on it and it refreshes data), it means that the scheduler is not blocked. OTOH if the observer is blocked, or very laggy, it would be an indication that something is indeed blocking the scheduler. You could then proceed by sprinkling IO.inspects to see where the blocking takes place (i.e. which operations take long to finish). Alternatively you could start the system with more schedulers, and use observer or Erlang tracing to deduce the same thing.

If the single thread processing doesn’t block the scheduler, the problem could be in how the library and/or SQLite handle concurrent operations. You could try the same experiment using two scheduler threads and two infinite processing loops. Again, sprinkling some IO.inspect for debugging purposes might help discover where the process is blocking.

In any case I feel that the sleep hack is not a reliable fix, and that the issue might still occasionally resurface, so I’d personally spend some time trying to understand the issue. It’s hard to tell exactly where the problem is, but based on your description, it might be caused by the NIF implementation of the 3rd party library, or by the concurrent behaviour of SQLite. Of course it’s also possible that you stumbled upon some bug/deficiency in Erlang, but I don’t think this is likely.

Either way, some further analysis & debugging is required to properly understand this. Best of luck and keep us posted :slight_smile:

tcoopman

tcoopman

What do you mean exactly with that it is blocking scheduling of other processes?

I’m not an expert at this, but the BEAM has preemptive scheduling which means that nothing should be able to block the scheduler for a long time

al2o3cr

al2o3cr

The scheduler’s designed to equitably share CPU between all the runnable processes; if it’s blocking that means something else is going wrong.

How many database connections are in Ecto’s pool? If there aren’t more than System.schedulers_online, that could cause the Tasks to hold all of them and then block forward progress from other processes.

Where Next?

Popular in Questions Top

Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
vac
Hi, I’m quite new in Elixir and I’m trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and I...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New

Other popular topics Top

sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 42920 311
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36128 110
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New

We're in Beta

About us Mission Statement