Skip to Main Content
  • Questions
  • Select for update in SQL Package with PRAGMA AUTONOMOUS_TRANSACTION

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, Vlad.

Asked: November 29, 2018 - 10:15 am UTC

Last updated: December 27, 2018 - 5:51 am UTC

Version: 12.1

Viewed 1000+ times

You Asked

Hello,

I have a package with some functions used to synchronize the operation performed by a number of servers.
When a server select an ID and perform operations on it the other servers can not select the same ID.
To achiev this i use a SYNC flag and a package. When a select operation is performed with that package i want to update the sync flag and then return a pipeline to the backend code.
The problem is that if 2 servers run the select on the package on the same time can select the same ID.
Because of that, i want to use a
SELECT FOR UPDATE
statment to lock to row in order to not be selected by other servers.
My problem is that every time i try to run the SELECT of the package i get this error:

ORA-06519: active autonomous transaction detected and rolled back
ORA-06512: at "DFEFRRTP1.COIN_PACKAGE", line 88
06519. 00000 -  "active autonomous transaction detected and rolled back"
*Cause:    Before returning from an autonomous PL/SQL block, all autonomous
           transactions started within the block must be completed (either
           committed or rolled back). If not, the active autonomous
           transaction is implicitly rolled back and this error is raised.
*Action:   Ensure that before returning from an autonomous PL/SQL block,
           any active autonomous transactions are explicitly committed
           or rolled back.


Here is a sample of my FUNCTION from the package BODY:

FUNCTION GET_MONITORING_ON_APPS
    RETURN MONITORING_ON_APPLICATIONS PIPELINED
  IS
    PRAGMA AUTONOMOUS_TRANSACTION;
    REC MONITORING_ON_RECORD;
    l_MONITORING_ON_RECORDS MONITORING_ON_RECORD;
    CURSOR MONITORING_ON_RECORDS
    IS
     SELECT A.ID FROM A
   JOIN B CH ON A.ID = B.ID AND B.STATUS_ID = 21
   JOIN C ON A.ID = C.ID AND TR.OTHER_ID = (SELECT MAX(OTHER_ID) FROM C WHERE OTHER_ID= A.ID) FOR UPDATE SKIP LOCKED;
  BEGIN
    OPEN MONITORING_ON_RECORDS;
    LOOP
      FETCH MONITORING_ON_RECORDS INTO l_MONITORING_ON_RECORDS;
      EXIT WHEN MONITORING_ON_RECORDS%NOTFOUND;
      
      UPDATE C
      SET SYNC_FLAG = 1,
        SYNC_TIMESTAMP = SYSTIMESTAMP
      WHERE CURRENT OF MONITORING_ON_RECORDS;
      PIPE ROW(l_MONITORING_ON_RECORDS);
    END LOOP;
    COMMIT;
    RETURN;
  END GET_MONITORING_ON_APPS;


I hope you can understand this... is my first question on this forum so take me easy :D
Thank you!


and Connor said...

When a server select an ID and perform operations on it the other servers can not select the same ID.


It sort out sounds like you re-inventing Advanced Queuing, which is a free component of the Oracle database. In the example you gave above, the style of operation would be along the lines of (pseudo-code)

- single process does:

for each ID in
( SELECT A.ID FROM A
   JOIN B CH ON A.ID = B.ID AND B.STATUS_ID = 21
   JOIN C ON A.ID = C.ID AND TR.OTHER_ID = (SELECT MAX(OTHER_ID) FROM C WHERE OTHER_ID= A.ID) FOR UPDATE SKIP LOCKED;
)
do
   dbms_aq.enqueue(id)
done


Then you can have as many concurrent processes as you like that do:

repeat forever
  msg := dbms_aq.dequeue
  do your processing on (msg.id) 
  commit;
end loop



In your case, it is the combination of "pipe row" and the autonomous transaction. Piping means that you are "returning" from the function before all of the rows are exhausted, hence the "active" transaction error - we are *still* in that cursor loop when we pass control back to the calling environment.

Even putting aside concurrency and skip locked, we can see that

SQL> create table t as
  2  select rownum x from dual connect
  3  by level <= 100;

Table created.

SQL>
SQL> create or replace
  2  function fnc return sys.odcinumberlist pipelined as
  3    pragma autonomous_transaction;
  4  begin
  5   for i in ( select * from t for update )
  6   loop
  7     pipe row ( i.x );
  8   end loop;
  9   commit;
 10   return;
 11  end;
 12  /

Function created.

SQL>
SQL> select * from fnc();
select * from fnc()
              *
ERROR at line 1:
ORA-06519: active autonomous transaction detected and rolled back
ORA-06512: at "MCDONAC.FNC", line 6
ORA-06512: at "MCDONAC.FNC", line 6


To enable what you want (without too much code rework) you can fetch the rows, tag them as belonging to your session, and then return them to the client, eg

SQL> create table t as
  2  select rownum x from dual connect
  3  by level <= 100;

Table created.

SQL>
SQL> alter table t add acquired_by number;

Table altered.

SQL>
SQL> create or replace
  2  function fnc return sys.odcinumberlist pipelined as
  3    pragma autonomous_transaction;
  4    id_list sys.odcinumberlist := sys.odcinumberlist();
  5  begin
  6   select x
  7   bulk collect into id_list
  8   from t
  9   where acquired_by is null
 10   for update skip locked ;
 11
 12   forall i in 1 .. id_list.count
 13     update t set acquired_by = sys_context('userenv','sid')
 14     where x = id_list(i);
 15     commit;
 16
 17   for i in 1 .. id_list.count
 18   loop
 19     pipe row (id_list(i));
 20   end loop;
 21   return;
 22  end;
 23  /

Function created.

SQL>
SQL> select * from fnc();

COLUMN_VALUE
------------
           1
           2
           3
           4
           5
           6
...
...
          98
          99
         100

100 rows selected.

SQL>


Now you can process the ID's you've retrieved. If you're looking to "load balance" in some way across multiple sessions, then you'd would add a limiter on the query, eg (rownum < 500 ) so multiple sessions can grab a batch of rows and work on them.

But AQ just sounds nicer to me.


Is this answer out of date? If it is, please let us know via a Comment

More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library