Re: [COMMITTERS] pgsql: Account for catalog snapshot in PGXACT->xmin updates.

From: Noah Misch <noah(at)leadboat(dot)com>
To: Robert Haas <robertmhaas(at)gmail(dot)com>
Cc: Tom Lane <tgl(at)sss(dot)pgh(dot)pa(dot)us>, "pgsql-hackers(at)postgresql(dot)org" <pgsql-hackers(at)postgresql(dot)org>, simon(at)2ndquadrant(dot)com
Subject: Re: [COMMITTERS] pgsql: Account for catalog snapshot in PGXACT->xmin updates.
Date: 2017-08-19 19:54:08
Message-ID: 20170819195408.GA4025245@rfd.leadboat.com
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-committers pgsql-hackers

On Tue, Dec 06, 2016 at 01:59:18PM -0500, Robert Haas wrote:
> On Tue, Dec 6, 2016 at 1:36 PM, Tom Lane <tgl(at)sss(dot)pgh(dot)pa(dot)us> wrote:
> > Robert Haas <robertmhaas(at)gmail(dot)com> writes:
> >> On Tue, Nov 15, 2016 at 3:55 PM, Tom Lane <tgl(at)sss(dot)pgh(dot)pa(dot)us> wrote:
> >>> Account for catalog snapshot in PGXACT->xmin updates.
> >
> >> It seems to me that this makes it possible for
> >> ThereAreNoPriorRegisteredSnapshots() to fail spuriously. The only
> >> core code that calls that function is in copy.c, and apparently we
> >> never reach that point with the catalog snapshot set. But that seems
> >> fragile.

I recently noticed this by way of the copy2 regression test failing, in 9.4
and later, under REPEATABLE READ and SERIALIZABLE. That failure started with
$SUBJECT. Minimal example:

CREATE TABLE vistest (a text);
BEGIN ISOLATION LEVEL REPEATABLE READ;
TRUNCATE vistest;
COPY vistest FROM stdin CSV FREEZE;
x
\.

Before $SUBJECT, the registered snapshot count was one. Since $SUBJECT, the
catalog snapshot raises the count to two.

> > Hm. If there were some documentation explaining what
> > ThereAreNoPriorRegisteredSnapshots is supposed to do, it would be possible
> > for us to have a considered argument as to whether this patch broke it or
> > not. As things stand, it is not obvious whether it ought to be ignoring
> > the catalog snapshot or not; one could construct plausible arguments

The rows COPY FREEZE writes will be visible (until deleted) to every possible
catalog snapshot, so whether CatalogSnapshot==NULL doesn't matter. It may be
useful to error out if a catalog scan is in-flight, but the registration for
CatalogSnapshot doesn't confirm or refute that.

> > either way. It's not even very obvious why both 0 and 1 registered
> > snapshots should be considered okay; that looks a lot more like a hack
> > than reasoned-out behavior. So I'm satisfied if COPY FREEZE does what

Fair summary. ThereAreNoPriorRegisteredSnapshots() has functioned that way
since an early submission[1], and I don't see that we ever discussed it
explicitly. Adding Simon for his recollection. I think the goal was to raise
an error if any scan of the COPY-target table might use an older CommandId;
this prevents a way of seeing tuples that the scan would not see after COPY
without FREEZE. Allowing one registered snapshot was a satisfactory heuristic
at the time. It rejected running plain queries, which have been registering
two snapshots. It did not reject otherwise-idle REPEATABLE READ transactions,
which register FirstXactSnapshot once; that has been okay, because any use of
FirstXactSnapshot in a query pushes or registers a copy. I think the code was
wrong to consider registered snapshots and ignore ActiveSnapshot, though. (We
must ignore COPY's own ActiveSnapshot, but it should be the only one.) I
don't blame $SUBJECT for changing this landscape; COPY should not attribute
meaning to a particular nonzero registered snapshot count.

> > it's supposed to do, which it still appears to do.
> >
> > IOW, I'm not interested in touching this unless someone first provides
> > adequate documentation for the pre-existing kluge here. There is no
> > API spec at all on the function itself, and the comment in front of the
> > one call site is too muddle-headed to provide any insight.
>
> COPY FREEZE is designed to be usable only in situations where the new
> tuples won't be visible earlier than would otherwise be the case.
> Thus, copy.c has a check that the relfilenode was created by our
> (sub)transaction, which guards against the possibility that other
> transactions might see the data early, and also a check that there are
> no other registered snapshots or ready portals, which guarantees that,
> for example, a cursor belonging to the current subtransaction but with
> a lower command-ID doesn't see the rows early. I am fuzzy why we need
> to check for both snapshots and portals, but the overall intent seems

The snapshot check helps in this case:

CREATE TABLE vistest (a text);
BEGIN ISOLATION LEVEL READ COMMITTED;
CREATE FUNCTION copy_vistest() RETURNS void LANGUAGE plpgsql
AS $$BEGIN COPY vistest FROM '/dev/null' CSV FREEZE; END$$;
CREATE FUNCTION count_vistest() RETURNS int LANGUAGE plpgsql STABLE
AS $$BEGIN RETURN (SELECT count(*) FROM vistest); END$$;
TRUNCATE vistest;
SELECT copy_vistest(), count_vistest();
ROLLBACK;

Other sessions still see rows they ideally would not see[2], the same kind of
anomaly the portal and snapshot tests exist to prevent. It's odd that we
protect visibility solely within the COPY transaction, the transaction most
likely to know what it's doing. I'm therefore skeptical of having these tests
at all, but I hesitate to remove them in back branches. Even in master, we
may want them later if we add visibility protection for other transactions.

I'm tempted to add this hack, ...

--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -2445,2 +2445,3 @@ CopyFrom(CopyState cstate)
{
+ InvalidateCatalogSnapshot();
if (!ThereAreNoPriorRegisteredSnapshots() || !ThereAreNoReadyPortals())

... along with comments and renaming ThereAreNoPriorRegisteredSnapshots().
That restores the pre-$SUBJECT semantics. I suspect those semantics allow
COPY FREEZE more than intended, but again that is mostly harmless. If we want
to make the restrictions more principled, ThereAreNoPriorRegisteredSnapshots()
could give way to a test like this (pseudocode):

n_relevant_registered_snap = cardinality(RegisteredSnapshots)
/* transaction/catalog snapshots are pushed/registered before use in query */
if (FirstXactSnapshot) n_relevant_registered_snap--
if (CatalogSnapshot) n_relevant_registered_snap--
/* expect one ActiveSnapshot for COPY itself */
can_freeze = cardinality(ActiveSnapshot) <= 1 && n_relevant_registered_snap == 0

Preferences?

[1] https://postgr.es/m/CA+U5nMJ8CdApEcm0+Ln0WXVx194k3wJHihb=-0xDHen1v0hhDg@mail.gmail.com
[2] https://postgr.es/m/20121206191823.GW5162@tamriel.snowman.net

> pretty clear to me. What are you confused about? It seems quite
> obvious to me that no matter how old the catalog snapshot is, it's
> nothing that we need to worry about here because it'll get invalidated
> and refreshed before use if anything changes meanwhile.

In response to

Responses

Browse pgsql-committers by date

  From Date Subject
Next Message Andres Freund 2017-08-20 18:23:19 pgsql: Change tupledesc->attrs[n] to TupleDescAttr(tupledesc, n).
Previous Message Tom Lane 2017-08-19 17:40:06 pgsql: Fix possible core dump in parallel restore when using a TOC list

Browse pgsql-hackers by date

  From Date Subject
Next Message Thomas Munro 2017-08-19 20:10:03 Re: Support for Secure Transport SSL library on macOS as OpenSSL alternative
Previous Message Alvaro Herrera 2017-08-19 19:48:43 Re: Proposal: global index