Thread: Locking to restrict rowcounts.

Locking to restrict rowcounts.

From
"Shakil Shaikh"
Date:
Hi,

Consider the following scenario:

CREATE FUNCTION test(name)
select into cnt count(id) from items where owner = name;
--suppose someone inserts during this point? then next check will succeed
when it should not.
if (cnt < 10) then
    insert into items values ('new item', name);
end;
end;

What is the best way to stop this function from entering too many items in a
concurrent context? I think a lock is needed, although I'm not sure which
would be most appropriate since the count requires the whole table (or at
least no adds to have occurred I think I read something about predicate
locking which sounds relevant but isn't supported in PostgreSQL. Ideally I
want some kind of lock only relevant to "name" above.

Any strategies to deal with this?

Shak


Re: Locking to restrict rowcounts.

From
Richard Huxton
Date:
Shakil Shaikh wrote:
> Hi,
>
> Consider the following scenario:
>
> CREATE FUNCTION test(name)
> select into cnt count(id) from items where owner = name;
> --suppose someone inserts during this point? then next check will
> succeed when it should not.
> if (cnt < 10) then
>    insert into items values ('new item', name);
> end;
> end;
>
> What is the best way to stop this function from entering too many items
> in a concurrent context? I think a lock is needed, although I'm not sure
> which would be most appropriate since the count requires the whole table

Well, you can't use FOR UPDATE with aggregates, otherwise you could do:
   SELECT into cnt count(id) FROM items WHERE owner = name FOR UPDATE;
So, you'd need:
   PERFORM * FROM items WHERE owner = name FOR UPDATE;

That won't stop someone blindly inserting into items, but it will act as
an effective lock on calling test() concurrently.

The other options would be:
1. Lock the relevant row in the users table (not so good if you have an
items2 table with similar requirements)
2. Advisory locks at the application level
3. Check after an insert on the items table and raise an exception if
there are 11+ items.

I'd be tempted by #3 - assuming most of the time you won't breach this
limit.

--
   Richard Huxton
   Archonet Ltd

Re: Locking to restrict rowcounts.

From
Richard Huxton
Date:
Glen Parker wrote:
> Richard Huxton wrote:

>> 3. Check after an insert on the items table and raise an exception if
>> there are 11+ items.
>>
>> I'd be tempted by #3 - assuming most of the time you won't breach this
>> limit.
>
> #3 won't work unless the other transactions have all committed by the
> time you do the check.  It is guaranteed to fail at some point.

If it's in an AFTER INSERT/UPDATE trigger then whatever transaction
takes you beyond 10 rows you will always get a failure. If you get a lot
of insert/delete pairings then you could spend a lot of time counting
rows, but that's all.

 > There would be nothing wrong with creating a table with rows that
 > exist solely for the purpose of locking.  This is a (usually) better
 > version of option #2 above.

Of course, if you're going to have a separate table then you might as
well store the count in there and actually update it on every
insert/update/delete. Assuming you might find the count of some use
somewhere. Set the fill-factor for the lock table and HOT should prevent
the table bloating too.

--
   Richard Huxton
   Archonet Ltd

Re: Locking to restrict rowcounts.

From
Glen Parker
Date:
Richard Huxton wrote:
> Shakil Shaikh wrote:
>> Hi,
>>
>> Consider the following scenario:
>>
>> CREATE FUNCTION test(name)
>> select into cnt count(id) from items where owner = name;
>> --suppose someone inserts during this point? then next check will
>> succeed when it should not.
>> if (cnt < 10) then
>>    insert into items values ('new item', name);
>> end;
>> end;
>>
>> What is the best way to stop this function from entering too many
>> items in a concurrent context? I think a lock is needed, although I'm
>> not sure which would be most appropriate since the count requires the
>> whole table
>
> Well, you can't use FOR UPDATE with aggregates, otherwise you could do:
>   SELECT into cnt count(id) FROM items WHERE owner = name FOR UPDATE;
> So, you'd need:
>   PERFORM * FROM items WHERE owner = name FOR UPDATE;
>
> That won't stop someone blindly inserting into items, but it will act as
> an effective lock on calling test() concurrently.
>
> The other options would be:
> 1. Lock the relevant row in the users table (not so good if you have an
> items2 table with similar requirements)
> 2. Advisory locks at the application level
> 3. Check after an insert on the items table and raise an exception if
> there are 11+ items.
>
> I'd be tempted by #3 - assuming most of the time you won't breach this
> limit.


#3 won't work unless the other transactions have all committed by the
time you do the check.  It is guaranteed to fail at some point.

In cases like this, in the unusual case that I don't already have some
suitable row locked, I just pick a row in some table that makes sense to
lock.  There would be nothing wrong with creating a table with rows that
exist solely for the purpose of locking.  This is a (usually) better
version of option #2 above.

-Glen


Re: Locking to restrict rowcounts.

From
Glen Parker
Date:
Richard Huxton wrote:
> Glen Parker wrote:
>> Richard Huxton wrote:
>>
>> #3 won't work unless the other transactions have all committed by the
>> time you do the check.  It is guaranteed to fail at some point.
>
> If it's in an AFTER INSERT/UPDATE trigger then whatever transaction
> takes you beyond 10 rows you will always get a failure. If you get a lot
> of insert/delete pairings then you could spend a lot of time counting
> rows, but that's all.


I still don't think it will work.  Two concurrent transactions could
still screw the data up.

Before: 9 rows.
Tx 1: See 9 rows, issue insert, see 10 rows.
Tx 2: See 9 rows, issue insert, see 10 rows.
After: 11 rows.

-Glen


Re: Locking to restrict rowcounts.

From
"Shakil Shaikh"
Date:
--------------------------------------------------
From: "Richard Huxton" <dev@archonet.com>
>
> Of course, if you're going to have a separate table then you might as well
> store the count in there and actually update it on every
> insert/update/delete. Assuming you might find the count of some use
> somewhere. Set the fill-factor for the lock table and HOT should prevent
> the table bloating too.
>

I think

PERFORM * FROM items WHERE owner = name FOR UPDATE;

sounds like it should work the best. What are the downsides for this that
would require the further table of counts? FWIW items has a SERIAL primary
key so FOR UPDATE should work on it.

Shak


Re: Locking to restrict rowcounts.

From
Richard Huxton
Date:
Just realised I didn't cc: the list on this. Was obviously having a good
evening.

Richard Huxton wrote:
> Glen Parker wrote:
>  > I still don't think it will work.  Two concurrent transactions could
>> still screw the data up.
>>
>> Before: 9 rows.
>> Tx 1: See 9 rows, issue insert, see 10 rows.
>> Tx 2: See 9 rows, issue insert, see 10 rows.
>> After: 11 rows.
>
> True, and well corrected.
>
> Serves me right for posting at the end of a long day. Particularly
> irritating as its almost identical to the example I've used myself when
> showing the limitations of PG's low-locking MVCC model.
>


--
   Richard Huxton
   Archonet Ltd