Reputation: 2600
I have credits
column that I need to check(if value > 0) and at the same time subtract 1 (multiple processes/connections will be bombarding db at the same time so it needs to be done in atomic fashion).
Is it enough to just start transaction and get + update row from there? Like:
Repo.transaction(fn ->
user = Repo.get(User, user_id)
if user.credits >= 1 do
MyRepo.update!(%{user | credits: user.credits - 1})
# ... If above works, create row in another table here.
end
end)
Does this put "lock" on user row? (imagine another process wanting to update credits in the middle of transaction above).
Upvotes: 0
Views: 746
Reputation: 222278
Does this put "lock" on user row? (imagine another process wanting to update credits in the middle of transaction above).
It does not. Here's a simple demonstration:
alias MyApp.{Repo, User}
import Ecto.{Changeset, Query}
user_id = Repo.insert!(%User{credits: 1}).id
pid = spawn(fn ->
user = Repo.get!(User, user_id)
caller = receive do caller -> caller end
Repo.update!(user |> change |> put_change(:credits, -5))
send(caller, :cont)
end)
Repo.transaction(fn ->
user = Repo.get!(User, user_id)
IO.inspect {:before, user.credits}
if user.credits >= 1 do
# Ask the other process to update a column.
send(pid, self)
# Wait until the other process is done.
receive do :cont -> :cont end
IO.inspect {:middle, Repo.get!(User, user_id).credits}
Repo.update!(user |> change |> put_change(:credits, user.credits - 1))
end
IO.inspect {:after, Repo.get!(User, user.id).credits}
end)
Output:
{:before, 1}
{:middle, -5}
{:after, 0}
One way to fix this is to obtain a row-level lock like FOR UPDATE
. This will ensure that no other changes are made to the same row until the current transaction is committed or aborted.
Changing
user = Repo.get!(User, user_id)
to
user = from(u in User, where: u.id == ^user_id, lock: "FOR UPDATE") |> Repo.one!
in the transaction in the above snippet will (rightfully) create a deadlock as the other process's SELECT will not return until the transaction is complete, and the transaction will not proceed until the other process gets the user.
So, in conclusion, you should replace:
user = Repo.get(User, user_id)
with
user = from(u in User, where: u.id == ^user_id, lock: "FOR UPDATE") |> Repo.one!
if you want to prevent updates to the returned user until your transaction is complete.
(Disclaimer: I'm not a Database expert but I believe what I've written above is correct. If anyone has any doubts or questions, please comment!)
Upvotes: 4