mbklein

mbklein

Sanity Check: Right way to use Ecto/Postgres locks (and how to test them)

We’ve got a kind of producer/consumer workflow happening, involving three processes.

The main process inserts a whole bunch of records into the progress table in a single atomic bulk insert. (This ensures that our entire roster of work gets set up at once without interruption.) Each progress row has a status of pending.

The Creator process is a GenServer that (basically) does the following on an interval:

def create_stuff(opts) do
  # Grab `batch_size` waiting entries
  batch = from(p in Progress, where: p.status == "pending", limit: opts[:batch_size], lock: "FOR UPDATE NOWAIT") |> Repo.all()
  # Set their status to "processing"
  batch |> Repo.update_all(set: [status: "processing", updated_at: now])
  # Process them and mark them completed all in a transaction
  batch
  |> Enum.each(fn entry -> 
    Repo.transaction(fn ->
      Logger.info("Creating #{entry.name}")
      do_something_with(entry)
      Progress.changeset(entry, %{status: "complete"}
    end)
  end)
end

The Redriver process is a GenServer that sweeps the progress table and resets any entry that’s been processing longer than @timeout seconds (and is therefore assumed to be part of a stuck batch).

timeout = DateTime.utc_now() |> DateTime.add(-@timeout, :second)
from(p in Progress, where: p.status == "processing" and p.updated_at <= ^timeout)
|> Repo.update_all(set: [status: "pending", updated_at: DateTime.utc_now()])

The idea is that no two Creator processes should ever select the same set of progress rows to work on, but of course there’s that tiny window between batch = and update_all where another process could grab the same rows. Hence the lock.

This seems to be working in practice, in that we’ve observed that work gets duplicated without the lock and doesn’t seem to with the lock. But we’ve had trouble devising a test for it.

The latest attempt is:

test "concurrency" do
  Sandbox.mode(Repo, {:shared, self()})
  
  1..5 
  |> Enum.each(fn index ->
    Progress.changeset(%Progress{}, %{name: "TEST_ENTRY_#{index}"}) |> Repo.insert()
  )

  log =
    capture_log(fn ->
      Enum.each(1..5, fn _ ->
        spawn(fn -> Creator.create_stuff(batch_size: 2) end)
      end)

  :timer.sleep(1000)

  assert from(p in Progress, where: p.status == "complete") |> Repo.aggregate(:count) == 5
  assert assert Regex.scan(~r/Creating TEST_ENTRY_1/, log) |> length() == 1
end

The above test fails, because "Creating TEST_ENTRY_1" appears in the log 5 times – once for each spawned call to Creator.create_stuff/1.

Now that I’ve got all of that out of the way, here are my questions:

  1. Is this the right way to use FOR UPDATE NOWAIT locking?
  2. Is is possible that it’s working in reality but the Ecto Sandbox is getting in the way testing it properly?
  3. Is there a better way to accomplish what we’re trying to do here?

Most Liked

mbuhot

mbuhot

I would expect FOR UPDATE SKIP LOCKED here.

The docs say:

With NOWAIT , the statement reports an error, rather than waiting, if a selected row cannot be locked immediately. With SKIP LOCKED , any selected rows that cannot be immediately locked are skipped.

Yes, I think sandbox is sharing a single transaction among all the spawned processes, instead of allowing separate connections/transactions for each one.

Where Next?

Popular in Questions Top

hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
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
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 127089 1222
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

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement