Achintya Srivastava
Achintya Srivastava

Reputation: 11

"original: error: tuple concurrently updated" postgres error while concurrently executing a plpgsql function -[ postgresSQL 14.5 ]

Hi stackoverflow community, this is my first ever question on stackoverflow, so I'd appreciate if you excuse my mistakes in presenting my question and really would appreciate any help.

I have written a plpgsql function that works as a middleware to check user permissions in my system, in which "function body" has an if-then-else block where "if" consists of a select statement that selects data from tables using joins and cast the response as 1 and 0 as integer if response = 1 then "then" block performs another select and return data as json if response = 0 then "else" block returns data as json.

plpgsql Function looks like as seen below:

create or replace function getUserData() returns json as 
$body$
    declare 
        v_belongs_to_group json;
        v_has_permission_in_group json;
    begin
        if (
            select case when exists(
                SELECT u.email , utg.id, g.group_name, g.id 
                    FROM users as u
                    inner join public.users_to_groups as utg on utg.user_id = u.id
                    inner join "groups" as g on g.id = utg.group_id 
                    where u.email='[email protected]' 
                    and g.group_name ='necessary group'
                    and g.status_id = 1
                ) then cast(1 as integer) else cast(0 as integer) end
        ) = 1 then
                select 
                    jsonb_build_object(
                        'user_id', u.id
                    )
                    from users as u 
                    inner join users_to_groups utg on utg.user_id=u.id 
                    inner join "groups" g on g.id = utg.group_id
                    inner join users_to_permissions utp on utp.user_group_id = utg.id 
                    inner join permissions p on p.id =utp.permission_id 
                    where u.email= '[email protected]' 
                    and g.group_name = 'another Group' 
                    and p."name" = 'some permission'
                    and g.status_id = 1
                    into v_has_permission_in_group;
                return to_json(v_has_permission_in_group);
            else
                v_belongs_to_group = jsonb_build_object('v_belongs_to_group',false);
                return to_json(v_belongs_to_group);
        end if;
    commit;
end; 
$body$ 
language plpgsql;

select * from getUserData() as fruit;

This works absolutely fine when I execute 1 at a time however if I perform parallelism test and concurrency tests I randomly receive error "original: error: tuple concurrently updated"

Complete error description:

DatabaseError [SequelizeDatabaseError]: tuple concurrently updated
        at Query.formatError (src\node_modules\sequelize\lib\dialects\postgres\query.js:366:16)
        at src\node_modules\sequelize\lib\dialects\postgres\query.js:72:18  
        at tryCatcher (src\node_modules\bluebird\js\release\util.js:16:23)  
        at Promise._settlePromiseFromHandler (src\node_modules\bluebird\js\release\promise.js:547:31)
        at Promise._settlePromise (src\node_modules\bluebird\js\release\promise.js:604:18)
        at Promise._settlePromise0 (src\node_modules\bluebird\js\release\promise.js:649:10)
        at Promise._settlePromises (src\node_modules\bluebird\js\release\promise.js:725:18)
        at _drainQueueStep (src\node_modules\bluebird\js\release\async.js:93:12)
        at _drainQueue (src\node_modules\bluebird\js\release\async.js:86:9) 
        at Async._drainQueues (src\node_modules\bluebird\js\release\async.js:102:5)
        at Async.drainQueues (src\node_modules\bluebird\js\release\async.js:15:14)
        at process.processImmediate (node:internal/timers:483:21) {
      parent: error: tuple concurrently updated
          at Parser.parseErrorMessage (src\node_modules\pg-protocol\dist\parser.js:287:98)
          at Parser.handlePacket (src\node_modules\pg-protocol\dist\parser.js:126:29)
          at Parser.parse (src\node_modules\pg-protocol\dist\parser.js:39:38)
          at Socket.<anonymous> (src\node_modules\pg-protocol\dist\index.js:11:42)
          at Socket.emit (node:events:520:28)
          at addChunk (node:internal/streams/readable:559:12)
          at readableAddChunkPushByteMode (node:internal/streams/readable:510:3)
          at Readable.push (node:internal/streams/readable:390:5)
          at TCP.onStreamRead (node:internal/stream_base_commons:191:23) {
        length: 90,
        severity: 'ERROR',
        code: 'XX000',
        detail: undefined,
        hint: undefined,
        position: undefined,
        internalPosition: undefined,
        internalQuery: undefined,
        where: undefined,
        schema: undefined,
        table: undefined,
        column: undefined,
        dataType: undefined,
        constraint: undefined,
        file: 'heapam.c',
        line: '4260',
        routine: 'simple_heap_update',
        sql: 'create or replace function getUserData() returns json as \n' +
          '        $body$\n' +
          '          declare \n' +
          '            belongs_to_default_group json;\n' +
          '            has_permission_in_group json;\n' +
          '          begin\n' +
          '            if (\n' +
          '              select case when exists(\n' +
          '                SELECT u.email , utg.id, g.group_name, g.id \n' +
          '                  FROM users as u\n' +
          '                  inner join public.users_to_groups as utg on utg.user_id = u.id\n' +
          '                  inner join "groups" as g on g.id = utg.group_id \n' +
          "                  where u.email='[email protected]' \n" +
          "                  and g.group_name ='necessary group'\n" +
          '                  and g.status_id = 1 for share\n' +
          '                ) then cast(1 as integer) else cast(0 as integer) end\n' +
          '            ) = 1 then\n' +
          '                    select \n' +
          '                      jsonb_build_object(\n' +
          "                        'user_id', u.id\n" +
          '                      )\n' +
          '                      from users as u \n' +
          '                      inner join users_to_groups utg on utg.user_id=u.id \n' +
          '                      inner join "groups" g on g.id = utg.group_id\n' +
          '                      inner join users_to_permissions utp on utp.user_group_id = utg.id \n' +
          '                      inner join permissions p on p.id =utp.permission_id \n' +
          "                      where u.email= '[email protected]' \n" +
          "                      and g.group_name ='another Group' \n" +
          `                      and p."name" = 'some permission'\n` +
          '                      and g.status_id = 1\n' +
          '                      into has_permission_in_group for share;\n' +
          '                    return to_json(has_permission_in_group);\n' +
          '                else\n' +
          "                    belongs_to_default_group = jsonb_build_object('belongs_to_default_group',false);\n" +
          '                    return to_json(belongs_to_default_group);\n' +
          '            end if;\n' +
          '            commit;\n' +
          '        end; \n' +
          '        $body$ \n' +
          '        language plpgsql;\n' +
          '        \n' +
          '        select * from getUserData() as fruit;',
        parameters: undefined
      },
      original: error: tuple concurrently updated
          at Parser.parseErrorMessage (src\node_modules\pg-protocol\dist\parser.js:287:98)
          at Parser.handlePacket (src\node_modules\pg-protocol\dist\parser.js:126:29)
          at Parser.parse (src\node_modules\pg-protocol\dist\parser.js:39:38)
          at Socket.<anonymous> (src\node_modules\pg-protocol\dist\index.js:11:42)
          at Socket.emit (node:events:520:28)
          at addChunk (node:internal/streams/readable:559:12)
          at readableAddChunkPushByteMode (node:internal/streams/readable:510:3)
          at Readable.push (node:internal/streams/readable:390:5)
          at TCP.onStreamRead (node:internal/stream_base_commons:191:23) {
        length: 90,
        severity: 'ERROR',
        code: 'XX000',
        detail: undefined,
        hint: undefined,
        position: undefined,
        internalPosition: undefined,
        internalQuery: undefined,
        where: undefined,
        schema: undefined,
        table: undefined,
        column: undefined,
        dataType: undefined,
        constraint: undefined,
        file: 'heapam.c',
        line: '4260',
        routine: 'simple_heap_update',
        sql: 'create or replace function getUserData() returns json as \n' +
          '        $body$\n' +
          '          declare \n' +
          '            belongs_to_default_group json;\n' +
          '            has_permission_in_group json;\n' +
          '          begin\n' +
          '            if (\n' +
          '              select case when exists(\n' +
          '                SELECT u.email , utg.id, g.group_name, g.id \n' +
          '                  FROM users as u\n' +
          '                  inner join public.users_to_groups as utg on utg.user_id = u.id\n' +
          '                  inner join "groups" as g on g.id = utg.group_id \n' +
          "                  where u.email='[email protected]' \n" +
          "                  and g.group_name ='necessary group'\n" +
          '                  and g.status_id = 1 for share\n' +
          '                ) then cast(1 as integer) else cast(0 as integer) end\n' +
          '            ) = 1 then\n' +
          '                    select \n' +
          '                      jsonb_build_object(\n' +
          "                        'user_id', u.id\n" +
          '                      )\n' +
          '                      from users as u \n' +
          '                      inner join users_to_groups utg on utg.user_id=u.id \n' +
          '                      inner join "groups" g on g.id = utg.group_id\n' +
          '                      inner join users_to_permissions utp on utp.user_group_id = utg.id \n' +
          '                      inner join permissions p on p.id =utp.permission_id \n' +
          "                      where u.email= '[email protected]' \n" +
          "                      and g.group_name ='another Group' \n" +
          `                      and p."name" = 'some permission'\n` +
          '                      and g.status_id = 1\n' +
          '                      into has_permission_in_group for share;\n' +
          '                    return to_json(has_permission_in_group);\n' +
          '                else\n' +
          "                    belongs_to_default_group = jsonb_build_object('belongs_to_default_group',false);\n" +
          '                    return to_json(belongs_to_default_group);\n' +
          '            end if;\n' +
          '            commit;\n' +
          '        end; \n' +
          '        $body$ \n' +
          '        language plpgsql;\n' +
          '        \n' +
          '        select * from getUserData() as fruit;',
        parameters: undefined
      },
      sql: 'create or replace function getUserData() returns json as \n' +
        '        $body$\n' +
        '          declare \n' +
        '            belongs_to_default_group json;\n' +
        '            has_permission_in_group json;\n' +
        '          begin\n' +
        '            if (\n' +
        '              select case when exists(\n' +
        '                SELECT u.email , utg.id, g.group_name, g.id \n' +
        '                  FROM users as u\n' +
        '                  inner join public.users_to_groups as utg on utg.user_id = u.id\n' +
        '                  inner join "groups" as g on g.id = utg.group_id \n' +
        "                  where u.email='[email protected]' \n" +
        "                  and g.group_name ='necessary group'\n" +
        '                  and g.status_id = 1 for share\n' +
        '                ) then cast(1 as integer) else cast(0 as integer) end\n' +
        '            ) = 1 then\n' +
        '                    select \n' +
        '                      jsonb_build_object(\n' +
        "                        'user_id', u.id\n" +
        '                      )\n' +
        '                      from users as u \n' +
        '                      inner join users_to_groups utg on utg.user_id=u.id \n' +
        '                      inner join "groups" g on g.id = utg.group_id\n' +
        '                      inner join users_to_permissions utp on utp.user_group_id = utg.id \n' +
        '                      inner join permissions p on p.id =utp.permission_id \n' +
        "                      where u.email= '[email protected]' \n" +
        "                      and g.group_name ='another Group' \n" +
        `                      and p."name" = 'some permission'\n` +
        '                      and g.status_id = 1\n' +
        '                      into has_permission_in_group for share;\n' +
        '                    return to_json(has_permission_in_group);\n' +
        '                else\n' +
        "                    belongs_to_default_group = jsonb_build_object('belongs_to_default_group',false);\n" +
        '                    return to_json(belongs_to_default_group);\n' +
        '            end if;\n' +
        '            commit;\n' +
        '        end; \n' +
        '        $body$ \n' +
        '        language plpgsql;\n' +
        '        \n' +
        '        select * from getUserData() as fruit;',
      parameters: undefined
    }

I tried adding "commit" explicitly thinking that there maybe uncommitted-reads performed in background by this function even so, when pgsql adds "commit" by default in all of its function.

Parallelism/Concurrency tests description:

I am simulating 20vu(vistrual users) to call an API endpoint over 1 min that results in almost 350 to 400 requests.

Code and API-server description: Responding to these requests is a node server that is executing this plpgsql function within a async-await function.

const requestHandler = async(ctx) =>{
   const result = await sequelize.query('plpgsql function as described above');
   if(result)  {ctx.body=result; ctx.status = 200}
   else {ctx.status=409}
}

Upvotes: 1

Views: 124

Answers (0)

Related Questions