Thread: BUG #2166: attempted to update invisible tuple

BUG #2166: attempted to update invisible tuple

From
"Euler Taveira de Oliveira"
Date:
The following bug has been logged online:

Bug reference:      2166
Logged by:          Euler Taveira de Oliveira
Email address:      eulerto@yahoo.com.br
PostgreSQL version: 8.1+
Operating system:   Slackware 10.1
Description:        attempted to update invisible tuple
Details:

I just execute the same transaction in 2 different backends and I got the
message: 'attempted to update invisible tuple'. In the first transaction I
execute: BEGIN - UPDATE and then open the other transaction and execute:
BEGIN - UPDATE so I commit the first one. In 8.0 it executes fine.

========== first transaction =========================
tst=# begin;
BEGIN
tst=# update produtos set estoque = estoque - itensmov.qtde from itensmov
where trim(itensmov.codigo) = 'C0001' and itensmov.fk_produto =
produtos.id_produto and produtos.controlarestoque = 'S';
UPDATE 21
tst=# commit;
COMMIT
tst=#
========== first transaction =========================

========== second transaction ========================
tst=# begin;
BEGIN
tst=# update produtos set estoque = estoque - itensmov.qtde from itensmov
where trim(itensmov.codigo) = 'C0003' and itensmov.fk_produto =
produtos.id_produto and produtos.controlarestoque = 'S';
ERRO:  attempted to update invisible tuple
tst=#
========== second transaction ========================

Re: BUG #2166: attempted to update invisible tuple

From
Tom Lane
Date:
"Euler Taveira de Oliveira" <eulerto@yahoo.com.br> writes:
> I just execute the same transaction in 2 different backends and I got the
> message: 'attempted to update invisible tuple'.

If you want this investigated then you'll need to provide a
self-contained example.  We have other things to do than guess about
the contents of your database...

            regards, tom lane

Re: BUG #2166: attempted to update invisible tuple

From
Euler Taveira de Oliveira
Date:
--- Tom Lane <tgl@sss.pgh.pa.us> escreveu:

> "Euler Taveira de Oliveira" <eulerto@yahoo.com.br> writes:
> > I just execute the same transaction in 2 different backends and I
> got the
> > message: 'attempted to update invisible tuple'.
>
> If you want this investigated then you'll need to provide a
> self-contained example.  We have other things to do than guess about
> the contents of your database...
>
Sorry for that. I didn't have the data yet. Some data to test it is in:
http://www.timbira.com/tst2.sql.gz

In 8.0 and previous I didn't reproduce it. Is it something related to
UPDATE ... FROM patch that I submitted?

I just execute the same transaction in 2 different backends and I got
the message: 'attempted to update invisible tuple'. In the first
transaction I execute: BEGIN - UPDATE and then open the other
transaction and execute: BEGIN - UPDATE so I commit the first one and
got that message.

========== first transaction =========================
tst=# begin;
BEGIN
tst=# update produtos set estoque = estoque - itensmov.qtde from
itensmov
where trim(itensmov.codigo) = 'C0001' and itensmov.fk_produto =
produtos.id_produto and produtos.controlarestoque = 'S';
UPDATE 21
tst=# commit;
COMMIT
tst=#
========== first transaction =========================

========== second transaction ========================
tst=# begin;
BEGIN
tst=# update produtos set estoque = estoque - itensmov.qtde from
itensmov
where trim(itensmov.codigo) = 'C0003' and itensmov.fk_produto =
produtos.id_produto and produtos.controlarestoque = 'S';
ERRO:  attempted to update invisible tuple
tst=#
========== second transaction ========================


Euler Taveira de Oliveira
euler[at]yahoo_com_br








_______________________________________________________
Yahoo! doce lar. Faça do Yahoo! sua homepage.
http://br.yahoo.com/homepageset.html

Re: BUG #2166: attempted to update invisible tuple

From
Tom Lane
Date:
Euler Taveira de Oliveira <eulerto@yahoo.com.br> writes:
> Sorry for that. I didn't have the data yet. Some data to test it is in:
> http://www.timbira.com/tst2.sql.gz

Thanks.

> In 8.0 and previous I didn't reproduce it.

Which versions did you try exactly?  I've reproduced it in 8.0.6 and HEAD.

> Is it something related to UPDATE ... FROM patch that I submitted?

No, it looks to me like it's a problem with this patch:

2005-08-19 20:39  tgl

    * src/: backend/access/heap/heapam.c, backend/commands/async.c,
    backend/commands/trigger.c, backend/commands/vacuum.c,
    backend/executor/execMain.c, backend/utils/time/tqual.c,
    include/access/heapam.h, include/access/htup.h,
    include/executor/executor.h, include/utils/tqual.h: Repair problems
    with VACUUM destroying t_ctid chains too soon, and with
    insufficient paranoia in code that follows t_ctid links.

I think EvalPlanQual is deciding that updated tuples are valid in
some cases where it shouldn't.  Unfortunately, if that's correct it
means that all the branches are broken since last August :-(

            regards, tom lane

Re: BUG #2166: attempted to update invisible tuple

From
Jaime Casanova
Date:
>
> I think EvalPlanQual is deciding that updated tuples are valid in
> some cases where it shouldn't.  Unfortunately, if that's correct it
> means that all the branches are broken since last August :-(
>
>                        regards, tom lane
>

i reproduced it in 8.1.1, so i guess you are right in tell that old
branches are broken...

--
regards,
Jaime Casanova
(DBA: DataBase Aniquilator ;)

Re: BUG #2166: attempted to update invisible tuple

From
Tom Lane
Date:
I wrote:
> I think EvalPlanQual is deciding that updated tuples are valid in
> some cases where it shouldn't.  Unfortunately, if that's correct it
> means that all the branches are broken since last August :-(

OK, here's the deal: the former coding of EvalPlanQual ignored (returned
NULL for) any tuple that was determined to have been updated by the
current transaction.  This is clearly wrong --- tuples updated by
previous commands of the current transaction should be visible for
further modification.  However, the new coding is also wrong --- tuples
already updated by the current command of the current transaction should
be ignored, and they aren't being.  The reason Euler's query is failing
is that there are multiple "itensmov" rows joining to some rows of
"produtos", leading to multiple attempts to update the row.

(BTW, this raises the question whether his query is really correct or
not, but I think we need to restore the previous behavior where the
first update attempt succeeds and additional updates in the same command
are just dropped.  This is what happens when EvalPlanQual is not entered
because there are no concurrent transactions, so we want to make the
concurrent case work the same.)

It looks to me like the correct test requires passing in the current
command ID so we can check the tuple's cmax against it.

            regards, tom lane

Re: BUG #2166: attempted to update invisible tuple

From
Tom Lane
Date:
I wrote:
> It looks to me like the correct test requires passing in the current
> command ID so we can check the tuple's cmax against it.

Er, cmin not cmax.  Here's the patch against 8.1 if you need it.

            regards, tom lane


Index: src/backend/commands/trigger.c
===================================================================
RCS file: /cvsroot/pgsql/src/backend/commands/trigger.c,v
retrieving revision 1.195.2.1
diff -c -r1.195.2.1 trigger.c
*** src/backend/commands/trigger.c    22 Nov 2005 18:23:07 -0000    1.195.2.1
--- src/backend/commands/trigger.c    12 Jan 2006 21:10:09 -0000
***************
*** 1736,1742 ****
                      epqslot = EvalPlanQual(estate,
                                             relinfo->ri_RangeTableIndex,
                                             &update_ctid,
!                                            update_xmax);
                      if (!TupIsNull(epqslot))
                      {
                          *tid = update_ctid;
--- 1736,1743 ----
                      epqslot = EvalPlanQual(estate,
                                             relinfo->ri_RangeTableIndex,
                                             &update_ctid,
!                                            update_xmax,
!                                            cid);
                      if (!TupIsNull(epqslot))
                      {
                          *tid = update_ctid;
Index: src/backend/executor/execMain.c
===================================================================
RCS file: /cvsroot/pgsql/src/backend/executor/execMain.c,v
retrieving revision 1.256.2.4
diff -c -r1.256.2.4 execMain.c
*** src/backend/executor/execMain.c    22 Nov 2005 18:23:08 -0000    1.256.2.4
--- src/backend/executor/execMain.c    12 Jan 2006 21:10:09 -0000
***************
*** 1219,1225 ****
                                  newSlot = EvalPlanQual(estate,
                                                         erm->rti,
                                                         &update_ctid,
!                                                        update_xmax);
                                  if (!TupIsNull(newSlot))
                                  {
                                      slot = newSlot;
--- 1219,1226 ----
                                  newSlot = EvalPlanQual(estate,
                                                         erm->rti,
                                                         &update_ctid,
!                                                        update_xmax,
!                                                        estate->es_snapshot->curcid);
                                  if (!TupIsNull(newSlot))
                                  {
                                      slot = newSlot;
***************
*** 1527,1533 ****
                  epqslot = EvalPlanQual(estate,
                                         resultRelInfo->ri_RangeTableIndex,
                                         &update_ctid,
!                                        update_xmax);
                  if (!TupIsNull(epqslot))
                  {
                      *tupleid = update_ctid;
--- 1528,1535 ----
                  epqslot = EvalPlanQual(estate,
                                         resultRelInfo->ri_RangeTableIndex,
                                         &update_ctid,
!                                        update_xmax,
!                                        estate->es_snapshot->curcid);
                  if (!TupIsNull(epqslot))
                  {
                      *tupleid = update_ctid;
***************
*** 1679,1685 ****
                  epqslot = EvalPlanQual(estate,
                                         resultRelInfo->ri_RangeTableIndex,
                                         &update_ctid,
!                                        update_xmax);
                  if (!TupIsNull(epqslot))
                  {
                      *tupleid = update_ctid;
--- 1681,1688 ----
                  epqslot = EvalPlanQual(estate,
                                         resultRelInfo->ri_RangeTableIndex,
                                         &update_ctid,
!                                        update_xmax,
!                                        estate->es_snapshot->curcid);
                  if (!TupIsNull(epqslot))
                  {
                      *tupleid = update_ctid;
***************
*** 1826,1831 ****
--- 1829,1835 ----
   *    rti - rangetable index of table containing tuple
   *    *tid - t_ctid from the outdated tuple (ie, next updated version)
   *    priorXmax - t_xmax from the outdated tuple
+  *    curCid - command ID of current command of my transaction
   *
   * *tid is also an output parameter: it's modified to hold the TID of the
   * latest version of the tuple (note this may be changed even on failure)
***************
*** 1835,1841 ****
   */
  TupleTableSlot *
  EvalPlanQual(EState *estate, Index rti,
!              ItemPointer tid, TransactionId priorXmax)
  {
      evalPlanQual *epq;
      EState       *epqstate;
--- 1839,1845 ----
   */
  TupleTableSlot *
  EvalPlanQual(EState *estate, Index rti,
!              ItemPointer tid, TransactionId priorXmax, CommandId curCid)
  {
      evalPlanQual *epq;
      EState       *epqstate;
***************
*** 1912,1917 ****
--- 1916,1939 ----
              }

              /*
+              * If tuple was inserted by our own transaction, we have to check
+              * cmin against curCid: cmin >= curCid means our command cannot
+              * see the tuple, so we should ignore it.  Without this we are
+              * open to the "Halloween problem" of indefinitely re-updating
+              * the same tuple.  (We need not check cmax because
+              * HeapTupleSatisfiesDirty will consider a tuple deleted by
+              * our transaction dead, regardless of cmax.)  We just checked
+              * that priorXmax == xmin, so we can test that variable instead
+              * of doing HeapTupleHeaderGetXmin again.
+              */
+             if (TransactionIdIsCurrentTransactionId(priorXmax) &&
+                 HeapTupleHeaderGetCmin(tuple.t_data) >= curCid)
+             {
+                 ReleaseBuffer(buffer);
+                 return NULL;
+             }
+
+             /*
               * We got tuple - now copy it for use by recheck query.
               */
              copyTuple = heap_copytuple(&tuple);
Index: src/include/executor/executor.h
===================================================================
RCS file: /cvsroot/pgsql/src/include/executor/executor.h,v
retrieving revision 1.120.2.1
diff -c -r1.120.2.1 executor.h
*** src/include/executor/executor.h    23 Nov 2005 20:28:05 -0000    1.120.2.1
--- src/include/executor/executor.h    12 Jan 2006 21:10:10 -0000
***************
*** 98,104 ****
  extern void ExecConstraints(ResultRelInfo *resultRelInfo,
                  TupleTableSlot *slot, EState *estate);
  extern TupleTableSlot *EvalPlanQual(EState *estate, Index rti,
!              ItemPointer tid, TransactionId priorXmax);

  /*
   * prototypes from functions in execProcnode.c
--- 98,104 ----
  extern void ExecConstraints(ResultRelInfo *resultRelInfo,
                  TupleTableSlot *slot, EState *estate);
  extern TupleTableSlot *EvalPlanQual(EState *estate, Index rti,
!              ItemPointer tid, TransactionId priorXmax, CommandId curCid);

  /*
   * prototypes from functions in execProcnode.c