2

I have a simple Postgres table which port numbers to listen on. If one port is used it can't be used again.

However due to multiple worker/threads they many times read simultaneously. I want that only one reader should be allowed at a time and then update the column value.

select port from socketport where available = 't'
update socketport set available = 'f' where port = port (from above)

Can above select statement be locked and then updated without releasing lock?

1
  • Why are you using the text values ‘t’ and ‘f’ instead of the boolean data type? Commented Sep 8, 2018 at 15:42

2 Answers 2

3

Edited: look at the last example is a better way

Yes, but you must send the two sentences within a transaction and add the "for update" clause in the select sentence.

Python

If you send then sentences one by one you can write something like this (in Python the connection handles transactions)

conn = psycopg2.connect("dbname=suppliers user=postgres password=postgres")
cur = conn.cursor()
cur.execute("select port from socketport where available = 't' limit 1 for update")
row = cur.fetchone()
port = row[0]
cur.close();
cur = conn.cursor()
cur.execute("update socketport set available = 'f' where port = %s", (port))
cur.close();
conn.commit();

node.js

If your language does not support transaction you must include manually:

const { Client } = require('pg')
const client = new Client(connOpts)
try{
  await client.query("BEGIN TRANSACTION");
  var result = await client.query("select port from socketport where available = 't' limit 1 for update");
  var port = result.rows[0].port;
  await client.query("update socketport set available = 'f' where port = $1", [port]);
  await client.query("commit");
} catch (e) {
  await client.query('ROLLBACK')
  throw e
} finally {
  client.release()
}    

pure postgresql

You can use direct postgres. Any block is within transaction

do $SQL$
declare
  v_port integer;
begin
  select port into v_port from socketport where available = 't' limit 1 for update;
  update socketport set available = 'f' where port = v_port;
end;
$SQL$;

looking for a better solution

using a function

You can do something like this. A postgres function that does the job and returns the port, then you can call the function from python (or whatever you want).

create or replace function get_avaialbe_port() returns integer
  language plpgsql
as $SQL$
declare
  v_port integer;
begin
  select port into v_port from socketport where available = 't' limit 1 for update;
  update socketport set available = 'f' where port = v_port;
  return v_port;
end;
$SQL$;

then in your backend you can call:

select get_avaialbe_port()

All code inside a function runs inside a transaction. The "for update" is stil needed, because without it two functions can run simultaneously and can take the same value of v_port prior to the update statement.

only one sentence

Also you can update the selected value in one sentence

update socketport 
  set available = 'f'
  where port in (select port from socketport where available = 't' limit 1)
  returning port;

You can call it directly from python or node.js. You can see a postgres running example at: http://rextester.com/OKMC68108

UPDATE sentence with RETURNING clause acts as SELECT you can query it and fetch the values. UPDATE ... RETURNING is like a SELECT that updates.

NOTES

Watch at LIMIT 1 in the examples. In the postgres version it is mandatory because of SELECT INTO. In python and node.js version it is recommended because in that way only one row is locked. Then the table can be updated with releasing ports or taking others using some kind of "order by random"

6
  • The SELECT FOR UPDATE is unnecessary if immediately followed by an UPDATE
    – user1822
    Commented Sep 3, 2018 at 13:10
  • @a_horse_with_no_name I do not agree with you. I believe that is necesary. The "for update" locks the row as garg10may needs. Are you shure that in python the two sentences are sended "immediately following"? I sure that in node.js may not! Are you sure that in pure postgresql two transactions may not run concurrently or if they do they do not interrupt SELECT/UPDATE pairs of immeediate sentences? postgresql.org/docs/9.5/static/… postgresql.org/docs/9.5/static/… Commented Sep 3, 2018 at 13:24
  • 1
    The UPDATE statement will lock the row the same way SELECT ... FOR UPDATE will. There is no need to do SELECT FOR UPDATE beforehand if you are doing an UPDATE immediately afterwards. Only doing the UPDATE is sufficient and less work for the database. If you are not even sure the two statements are sent "immediately" after another sending only a single statement is even more important.
    – user1822
    Commented Sep 3, 2018 at 13:32
  • If you do not do the "FOR UPDATE" can hapens this: The SELECT obtains port number 77, concurrently in other transaction the SELECT obtains again the number 77, then in the first transaction the UPDATE locks the record and sets avaialbe='f' then the other transaction do the same upates the same record seting avaiable='f'. If you put "FOR UPDATE" when the second SELECT want to get the avaiable port "FOR UPDATE" must wait that the first transaction commits; then the SELECT was executed and the next avaiable port was getted. Commented Sep 3, 2018 at 13:44
  • 1
    @EmilioPlatzer: if both transactions were only using UPDATE, the same thing would happen. Plus: Gerard is right: an `update ... returning" would make the select unnecessary
    – user1822
    Commented Sep 3, 2018 at 20:00
0

I found the proper way in python is through transaction. For that commit needs to be made after both select and update also for update in select is necessary, otherwise it won't work. conn.commit() or cursor.execute('commit') any can be called to commit the transaction.

try:
    conn = psycopg2.connect(database='tanmay_db', user='xx', host='xx', port='5432', password='xx')
    cur = conn.cursor()
    cur.execute("""select port from ports where is_available = 't' for update""")
    row  = cur.fetchone()
    port = row[0]
    cur.execute("""update ports set is_available='f' where port = %s"""%port)
    cur.execute('commit')
    #conn.commit()
    cur.close()
except psycopg2.DatabaseError as e:
    print(e)
finally:
    if conn is not None:
        conn.close()

You can also close the cursor after select statement and re-open it again. but commit necessarily needs to be made after both the statement, If commit is ignored select would always return the same port.

1
  • You are right I forgot to copy the last line (commit). Your question is about the need of lock. Both answers are the same in that point. But if you are looking for the best option I add a suggestion in my answer. Commented Sep 4, 2018 at 13:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.