[erlang-questions] Couple of questions about mnesia locking

131 views
Skip to first unread message

ryeguy

unread,
Apr 3, 2009, 3:34:53 PM4/3/09
to erlang-q...@erlang.org
If I wanted to check if a username is in use before registering an
account like this:

F=fun()->
case is_username_available(User) of %% does a mnesia:read to see if
there is a record with that username
false -> throw(username_in_use);
true -> mnesia:write(User)
end,

mnesia:transaction(F).

Don't I have a potential race condition here? Since there is no record
to lock because it doesn't exist, how can mnesia guarantee another
process isn't also going to write that same Username to the database?
If I'm doing this wrong, what's the right way? A table lock (eww)?

My second question is regarding a quote from the mnesia manual:
"Write locks are normally acquired on all nodes where a replica of the
table resides (and is active). Read locks are acquired on one node
(the local one if a local replica exists). "

What does it mean read locks are acquired on one node? What would
happen when a table is distributed? Wouldn't that defeat the purpose?
_______________________________________________
erlang-questions mailing list
erlang-q...@erlang.org
http://www.erlang.org/mailman/listinfo/erlang-questions

Richard Andrews

unread,
Apr 4, 2009, 7:31:26 AM4/4/09
to ryeguy, erlang-q...@erlang.org


> If I wanted to check if a username is in use before registering an
> account like this:
>
> F=fun()->
> case is_username_available(User) of %% does a mnesia:read to see if
> there is a record with that username
> false -> throw(username_in_use);
> true -> mnesia:write(User)
> end,
>
> mnesia:transaction(F).
>
> Don't I have a potential race condition here? Since there is no record
> to lock because it doesn't exist, how can mnesia guarantee another
> process isn't also going to write that same Username to the database?
> If I'm doing this wrong, what's the right way? A table lock (eww)?

You could use a gatekeeper - a singleton process on only one node which is allowed to create usernames. Creators make a request to the gatekeeper which serialises the requests and therefore prevents the race. The problem then shifts to process registration consistency and takeover in the case of the gatekeeper failure.

Sharding/splitting the gatekeeper responsibility by eg. username first letter would quarantine the damage from a gatekeeper failure.

> My second question is regarding a quote from the mnesia manual:
> "Write locks are normally acquired on all nodes where a replica of the
> table resides (and is active). Read locks are acquired on one node
> (the local one if a local replica exists). "
>
> What does it mean read locks are acquired on one node? What would
> happen when a table is distributed? Wouldn't that defeat the purpose?

A read lock on any node prevents a write lock from being acquired on that item.
A write lock must be acquired on *all* nodes so it is sufficient to take a read lock on any one node to block a write lock from being acquired.

--
Rich


Enjoy a safer web experience. Upgrade to the new Internet Explorer 8 optimised for Yahoo!7. Get it now.

ryeguy

unread,
Apr 4, 2009, 1:54:44 PM4/4/09
to erlang-q...@erlang.org
Hmm..well what if we had a transaction where we add to a bank account
balance. You obviously read the balance then increment it.

If NodeA and NodeB both have replicas of the table containing the
balance, couldn't this happen:

NodeA reads balance of 5
NodeB reads balance of 5
NodeA writes balance of 10
NodeB writes balance of 15

Since there is no read lock on each node? I'm assuming it would work
like this because, if I understand correctly, the locks are acquired
AS the transaction processes, and not the second the transaction fun
is executed, right?

In this situation, is the solution to simply just grab a write lock on
the record when reading it?
This poses another question: how do you acquire a write lock on a
record when you do an index read?

> erlang-questi...@erlang.orghttp://www.erlang.org/mailman/listinfo/erlang-questions

Hynek Vychodil

unread,
Apr 4, 2009, 3:59:11 PM4/4/09
to ryeguy, erlang-q...@erlang.org
Write operation will require aquiring write lock, obviously.
--
--Hynek (Pichi) Vychodil

Analyze your data in minutes. Share your insights instantly. Thrill your boss.  Be a data hero!
Try Good Data now for free: www.gooddata.com

Ryan Lepidi

unread,
Apr 4, 2009, 4:14:12 PM4/4/09
to Hynek Vychodil, erlang-q...@erlang.org
I know a write lock would eventually be acquired, but aren't locks acquired AS the functions in the transaction are called? Like wouldn't it do this:

NodeA reads balance of 5; gets read lock on record
NodeB reads balance of 5; gets read lock on record
NodeA writes balance of 10; gets write lock on record
NodeB writes balance of 15; gets write lock on record
^ Obviously this would be bad

Or does mnesia read the entire transaction first and then apply all of the locks before doing any of the actions? I don't think this is the case because then functions such as mnesia:wread would be necessary.

Hynek Vychodil

unread,
Apr 4, 2009, 4:52:06 PM4/4/09
to Ryan Lepidi, erlang-q...@erlang.org
mnesia uses optimistic locking. When NodeB's write is called than transaction fails and is restarted. This is why mneasia transaction should not contain side-effects. See mnesia manual for details.

Ulf Wiger

unread,
Apr 5, 2009, 3:27:50 AM4/5/09
to Hynek Vychodil, Ryan Lepidi, erlang-q...@erlang.org
Hynek Vychodil wrote:
> mnesia uses optimistic locking. When NodeB's write is called than
> transaction fails and is restarted. This is why mneasia transaction
> should not contain side-effects. See mnesia manual for details.

Actually, no. Mnesia waits for the locks needed before
proceeding, and keeps the locks until it either aborts
or commits. However, in any locking environment, one
must guard against deadlocks. If the lock manager is
centralized, it's possible to maintain a wait-for graph,
which is scanned each time a new lock is requested.

This approach doesn't scale in a distributed setting,
so an alternative approach, called "deadlock prevention",
is to allow only unidirectional dependencies (for some
definition of unidirectional - e.g. from smaller to
larger pids.) If a lock is requested that would create
a dependency in the other direction, one of the
transactions involved is restarted. This is why mnesia
transactions can restart sometimes.

BR,
Ulf W
--
Ulf Wiger
CTO, Erlang Training & Consulting Ltd
http://www.erlang-consulting.com

Ryan Lepidi

unread,
Apr 5, 2009, 1:53:25 PM4/5/09
to Ulf Wiger, erlang-q...@erlang.org
I apreciate your guys' help. I have a question though, what would happen in this situation?


NodeA reads balance of 5; gets read lock on record

NodeB reads balance of 5; gets read lock on record   %on another node, so read lock is possible
NodeB writes balance of 15; gets write lock on record %does the write then releases the lock


NodeA writes balance of 10; gets write lock on record

NodeA would have a stale value and NodeB would not have to wait for any locks, so neither of them would restart afaik. I know one way to prevent this is to acquire a write lock at the beginning, but how can you do that with an index_read? It seems the only way to do that would be to read the object then wread() it again with the object's primary key. Blech. Hopefully there is some other solution?

Actually, after reading Ulf's reply, I just realized what I typed is probably answered by what he said. Do you mean ALL locks are acquired before any records are read or written? So in other words, the situation above is impossible?

Ulf Wiger

unread,
Apr 5, 2009, 5:21:46 PM4/5/09
to Ryan Lepidi, erlang-q...@erlang.org
A read lock can be upgraded to a write lock /only/ if there are no other read locks on the object. Otherwise the transaction must wait until the other read locks are released. In your example, this won't happen, and one of the two transactions must restart. You can go for a write lock directly if you want. For index_read(), there isn't a version that lets you specify the lock type, but according to the docs, it takes a read lock on the whole table. Then start by taking a write lock on the table instead.

BR,
Ulf W
-- originalmedd. --
Ämne: Re: [erlang-questions] Couple of questions about mnesia locking
Från: Ryan Lepidi <rye...@gmail.com>
Datum: 2009.04.05 19.51

_______________________________________________

Ryan Lepidi

unread,
Apr 4, 2009, 4:16:20 PM4/4/09
to Hynek Vychodil, erlang-q...@erlang.org
I meant to say unnecessary*

Thomas Lindgren

unread,
Apr 6, 2009, 1:55:59 PM4/6/09
to erlang-q...@erlang.org
Your question has more to do with transactional systems in general than mnesia. Your example is not "serializable" and so wouldn't be permitted in a transactional database. As far as I can see, NodeA and NodeB would instead deadlock, and one of them would be aborted and perhaps retried.

Regarding how this is implemented (though perhaps mnesia does things differently), before acquiring a write lock on the balance record, NodeB would first have to wait for NodeA to release its read lock on the same, and conversely, NodeA would wait for all read-lockers of the record to release their locks before acquiring its write lock. A simple way of getting things right is for a transaction to only releases its locks when it commits or aborts. (It's up to the database to detect and repair deadlocks.)

Best,
Thomas


From: Ryan Lepidi <rye...@gmail.com>
To: Ulf Wiger <ulf....@erlang-consulting.com>
Cc: erlang-q...@erlang.org
Sent: Sunday, April 5, 2009 7:53:25 PM
Subject: Re: [erlang-questions] Couple of questions about mnesia locking
Reply all
Reply to author
Forward
0 new messages