mrnovalles

mrnovalles

Oban Pro 1.5.0 migration locked our production table

We’re proud and happy users of Oban.Pro , however in the most recent upgrade to v1.5.0, the migrations ended up locking the oban_jobs table for over 10 mins. This caused our production app to fail on incoming requests.
Our Oban.Pruner deletes jobs older than 1 month for auditing reasons, and we therefore have ~2 million jobs in completed state at any given time.We identified the problematic bit of the migration as a generated column.

This SQL needs a table lock and a sequential scan of the table in order to calcule the value of the GENERATED uniq_key column.
We have sucessfully migrated to v1.5.0, since we were able to back up the table and trim it down to 50_000 rows, which ended up taking ~10 seconds for the snippet above to run.
Sharing this as it might be a pitfall for somebody else upgrading to Oban Pro v1.5.0 and to get a conversation started if you have some thoughts or ideas.

Tags

oban-chat

Thread Titles:

For anyone attempting to migrate to Oban Pro v1.5.0 with a huge oban_jobs table, please be aware of a possible table lock

Marked As Solved

milmazz

milmazz

Yes, with this particular Oban DB Migration, you should be careful or take some steps before proceeding.

Essentially, we decided to split the Oban Pro Ecto migration into multiple steps, and mostly do manual migrations, following the pattern suggested in the article Automatic and manual Ecto migrations written by the folks from Dashbit.

But, first, you have to understand what these DB schema migrations entail, I will not post the code here out of respect to the Oban authors, and also because you can easily find it at lib/oban/pro/migrations/v1_5_0.ex. But, in general lines, there are 5 big steps:

  1. Creates a UDF or user-defined function that essentially translates Oban states into bits. For example, the scheduled state is 0, the available state is 1, and so on. This is safe to apply even as an automatic Ecto migration.
  2. An ALTER TABLE in the oban_jobs table that will add a generated column uniq_key; this column is special because its value will always be computed from other columns or expressions. This step must be applied in a manual migration after reducing the number of rows as much as possible because it will involve a sequential operation and Access Exclusive lock (the strictest one), as much of the ALTER TABLE operations. If you want to inspect all the locks for this query, check the wonderful Safe Ecto Migrations series from David Bernheisel.
  3. It will add a new partial UNIQUE INDEX against the previous column uniq_key, this index is not concurrent.
  4. It will add a new partial index for batches, this index is not concurrent.
  5. It will add a new partial index for chains, this index is not concurrent.

Once we understand these 5 steps and the problems that could arise when you have millions of rows in the oban_jobs table, you can take some actions. The first thing we did was to take advantage of the DynamicPruner to reduce our policy retention and be more aggressive, as you, we have to keep the records around, thankfully, the DynamicPruner provides an option to set up a callback before deleting the records, in that callback, you can perfectly copy the jobs that are about to be deleted into cold storage or another table, your choice.

But while we were on this pruning crusade, we discovered, and then confirmed with the Oban authors, that the DynamicPruner plugin uses the scheduled_at field as a limit, which is fine, the “problem” is that we use a lot of Oban Workflows, and there is a workaround that sets the scheduled_at field way ahead in the future as a way to put “on hold” the workflow dependencies without introducing a new state like available, scheduled, etc. So, you can opt to create your own plugin that considers this case too, allowing you to prune even more records. This bug in the DynamicPruner is fixed in the most recent stable (v1.5.2) release, but at this point, we were using v1.4.14.

Once you have reduced the size of the oban_jobs table, it should be safe to apply the manual migration for step 2, this preferably should be applied at a time when you don’t have too much load in your system, probably during a weekend would be a good idea.

While I understand why the Oban authors decided to avoid creating these indexes concurrently, you must understand that creating these 3 indexes will lock the table for writes (not reads), preventing concurrent inserts, updates, or deletes until the index it’s done. So, we decided to wrap these index creations into their own manual migration and set the option concurrently: true, considering that you must set the module attributes to disable DDL transactions and the migration lock.

Keep in mind that applying the UNIQUE INDEX in the uniq_key column could fail. This means you have duplicate entries for this column, and you have to solve that issue first, and try again.

Once you have applied all these migrations, it should be safe to proceed with the Oban upgrade to v1.5.

Also Liked

sorenone

sorenone

Oban Core Team

Hi @mrnovalles :surfing_woman:Thank you for posting this in the forum. We’re sure it will help others. We’ll be sure to add some warnings to the documentation.

apoorv-2204

apoorv-2204

The amazing community

fel-mazo

fel-mazo

Hi ! just wanted to point out that a warning is present on the migration guide to 1.6, but missing from 1.5
it would also be nice if 1.5 got updated to support the ability to separate the index migrations from the schema ones… after all the code is already in 1.6.

Thanks a lot, and thank you @milmazz for your detailed answer ! very helpful.

Where Next?

Popular in Questions Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
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
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
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
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Other popular topics Top

TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41989 114
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
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
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement