views:

468

answers:

4

I have a table in mnesia and I need to update individual fields in the records in it. According to http://stackoverflow.com/questions/1820996/erlang-mnesia-updating-a-single-field-value-in-a-row if I do something like:

update_a(Tab, Key, Value) ->
  fun() ->
    [P] = mnesia:wread({Tab, Key}),
    mnesia:write(Tab, P#rec{a=Value}, write)
  end.

Now as I understand, the above code reads a record P based on a Key, acquiring a write lock on the record, so that no other transactions modify this record while it is being read and written back (or in short, updated). So far so good.

Now my requirement is that I need to able to read records based on both the Key and one other field in the table and then perform an update on it. A function that will do this looking up is mnesia:match_object. The problem now is that, the function only supports a read lock, not a write lock, according to http://www.erlang.org/doc/man/mnesia.html#match_object-3.

The consequence of this is that, suppose in the above function I were to use mnesia:match_object, I will get a (group of) record(s), all with read locks. After I read the records, I need to perform some checks on the retrieved data and then write back the updated record only if a condition is satisfied. Now, assume there are two parallel transactions T1 and T2 initiated by two different sources running. Both T1 and T2 access the same record, at the same time. Since they are read locked, both T1 and T2 will be able to read the records parallely. Both T1 and T2 will perform the same check on the same record, and if the condition matches, both will proceed to execute the update. But, in my code, if T1 and T2 were to have executed serially, T1 would have made changes to the record and in T2, it would have read these changed records and the condition would have failed and no update would have been made.

In short, I need to write lock records that are returned by mnesia:match_object. The documentation clearly states only read lock is supported. Are there are any alternatives?

UPDATE: I've been experimenting a little bit, and a possible solution I thought could be to use compound keys. Suppose I have data written to a table like:

mnesia:transaction(fun() -> mnesia:write(mytable, #rec{i={1,2}, a=2, b=3}, write) end).

Is there any way to lookup entries, using don't cares?

I tried these, but both returned empty results:

mnesia:transaction(fun()-> mnesia:read(mytable, {1,'_'}, read) end).
mnesia:transaction(fun()-> mnesia:read(mytable, {1,_}, read) end).
A: 

Can't you just lock the table in your transaction beforehand with mnesia:write_lock_table(Tab) ?

Zed
I considered this, but I just need to update and hence lock one record. The table will be used by several clients simultaneously, and locking the entire table for just one record updation will reduce the throughput.
ErJab
+2  A: 

Even though it returns them under a read lock, you can still update them with write afterwards. The whole read/wread thing was just an optimization.

You can use an ordered_set table to implement this mnesia-trick on compound keys.

Christian
My application requires concurrent access to the table by several clients. If I were to first use mnesia:match_object to get the read locked records and then update the lock on the records, in between the time I fetch the records with match_object and upgrade to a write lock, isn't there a possibility of another transaction reading the the records using mnesia:match_object, given my concurrent access requirements?
ErJab
No, the entire mnesia:transaction is atomic and it sees a consistent version of the data. The transaction will be retried a number of times until the commit goes through.
Christian
+1  A: 

You don't have to worry about it. From the mnesia documentation:

Read locks may be shared, which means that if one transaction manages to acquire a read lock on an item, other transactions may also acquire a read lock on the same item. However, if someone has a read lock no one can acquire a write lock at the same item. If some one has a write lock no one can acquire a read lock nor a write lock at the same item.

If a transaction has a read lock on an object, that object can't be edited by another transaction.

Say you have two transactions, T1 and T2, which are executing in parallel:

  1. T1 does mnesia:match_object, and acquires a read lock on all the returned objects.
  2. T2 does an equivalent mnesia:match_object, and acquires a read lock on the same objects.
  3. T2 attempts to acquire a write lock on of the objects (to edit it).
  4. Mnesia automatically aborts T2, to be retried later.
  5. T1 acquires a write lock on the objects, and edits them.
  6. T1 finishes.
  7. Mnesia retries T2.

Note that T2 may be retried several times, depending on how long T1 takes to complete (ie. release its locks).

According to my tests, the locking behavior of mnesia:match_object isn't consistent. For example, mnesia:match_object(mytable, {mytable, 2, '_'}, LockType) will lock only the record with Key 2, but mnesia:match_object(mytable, {mytable, '_', test}, LockType) locks the entire table.

Also note that the documentation isn't correct, mnesia:match_object(Table, Pattern, write) does work, and seems to follow the same pattern as 'read', ie. if you specify the key, only the matching record will be write-locked; if you don't specify the key, the entire table will be write-locked.

You can test it yourself by doing something like this:

test() ->
    mnesia:transaction(fun()-> 
        Table = mytable,
        Matches = mnesia:match_object(Table, {Table, 2, '_'}, write),
        io:format("matched: ~p~n", [Matches]),
        spawn(fun()->mnesia:transaction(fun()->
                        io:format("trying to read~n",[]),
                        io:format("read: ~p~n", [mnesia:read(Table, 2, read)])
                        end) end),        
        timer:sleep(1000),
        RereadMatches = lists:map(fun(#mytable{id=Id}) -> mnesia:read(Table, Id, write) end, Matches),
        io:format("reread matches: ~p~n", [RereadMatches])
        end).

By changing the pattern and lock type passed to match_object, and the key number and lock type passed to mnesia:read in the spawned process (or by using mnesia:write), you can test the various locking behaviors.

Addendum: See this post by Ulf Wiger on the same topic.

Addendum 2: See the section on "Isolation" in the Mnesia user guide.

Edit: The above was all done on a set-type table, match_object locking behavior might be different on a bag-type table.

sb
That made a lot of sense to me, thanks!
ErJab
A: 

Like this,

        case mnesia:match_object(#rec{name=Name, _='_'}) of
            [] -> not_found;
            [First|Rest] -> something
        end,
humasect