Hi all Following on from
bigint txids vs 'xid' type, new txid_recent(bigint) => xid https://www.postgresql.org/message-id/camsr+yfdzmn_iz7krroe+j0kvlqvfvgvzxbcvxr-mljgtoz...@mail.gmail.com here's a patch that implements a txid_status(bigint) function to report the commit status of a function. If an application is disconnected while a COMMIT request is in flight, the backend crashes mid-commit, etc, then an application may not be sure whether or not a commit completed successfully or was rolled back. While two-phase commit solves this it does so at a considerable overhead, so introduce a lighter alternative. txid_status(bigint) lets an application determine the status of a a commit based on an xid-with-epoch as returned by txid_current() or similar. Status may be committed, aborted, in-progress (including prepared xacts) or null if the xact is too old for its commit status to still be retained because it has passed the wrap-around epoch boundary. Applications must call txid_current() in their transactions to make much use of this since PostgreSQL does not automatically report an xid to the client when one is assigned. A future protocol enhancement to report txid assignment would be very useful, but quite separate to this. -- Craig Ringer http://www.2ndQuadrant.com/ PostgreSQL Development, 24x7 Support, Training & Services
From a298bb242716fbf1526bf9f949a94740a9d975fc Mon Sep 17 00:00:00 2001 From: Craig Ringer <cr...@2ndquadrant.com> Date: Fri, 19 Aug 2016 14:44:15 +0800 Subject: [PATCH 1/5] Introduce txid_status(bigint) to get status of an xact If an appliation is disconnected while a COMMIT request is in flight, the backend crashes mid-commit, etc, then an application may not be sure whether or not a commit completed successfully or was rolled back. While two-phase commit solves this it does so at a considerable overhead, so introduce a lighter alternative. txid_status(bigint) lets an application determine the status of a a commit based on an xid-with-epoch as returned by txid_current() or similar. Status may be committed, aborted, in-progress (including prepared xacts) or null if the xact is too old for its commit status to still be retained because it has passed the wrap-around epoch boundary. Applications must call txid_current() in their transactions to make much use of this since PostgreSQL does not automatically report an xid to the client when one is assigned. --- doc/src/sgml/func.sgml | 11 ++++- src/backend/access/transam/clog.c | 23 ---------- src/backend/catalog/system_views.sql | 20 +++++++++ src/backend/utils/adt/txid.c | 82 ++++++++++++++++++++++++++++++++++++ src/include/access/clog.h | 23 ++++++++++ src/include/catalog/pg_proc.h | 2 + src/test/regress/expected/txid.out | 50 ++++++++++++++++++++++ src/test/regress/sql/txid.sql | 36 ++++++++++++++++ 8 files changed, 223 insertions(+), 24 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 7830334..c2a0aeb 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16911,6 +16911,10 @@ SELECT collation for ('foo' COLLATE "de_DE"); <primary>txid_visible_in_snapshot</primary> </indexterm> + <indexterm> + <primary>txid_status</primary> + </indexterm> + <para> The functions shown in <xref linkend="functions-txid-snapshot"> provide server transaction information in an exportable form. The main @@ -16929,7 +16933,7 @@ SELECT collation for ('foo' COLLATE "de_DE"); <row> <entry><literal><function>txid_current()</function></literal></entry> <entry><type>bigint</type></entry> - <entry>get current transaction ID, assigning a new one if the current transaction does not have one</entry> + <entry>get current 64-bit transaction ID with epoch, assigning a new one if the current transaction does not have one</entry> </row> <row> <entry><literal><function>txid_current_snapshot()</function></literal></entry> @@ -16956,6 +16960,11 @@ SELECT collation for ('foo' COLLATE "de_DE"); <entry><type>boolean</type></entry> <entry>is transaction ID visible in snapshot? (do not use with subtransaction ids)</entry> </row> + <row> + <entry><literal><function>txid_status(<parameter>bigint</parameter>)</function></literal></entry> + <entry><type>txid_status</type></entry> + <entry>report the status of the given xact - committed, aborted, in-progress, or null if the xid is too old</entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/access/transam/clog.c b/src/backend/access/transam/clog.c index 2634476..1a6e26d 100644 --- a/src/backend/access/transam/clog.c +++ b/src/backend/access/transam/clog.c @@ -41,29 +41,6 @@ #include "miscadmin.h" #include "pg_trace.h" -/* - * Defines for CLOG page sizes. A page is the same BLCKSZ as is used - * everywhere else in Postgres. - * - * Note: because TransactionIds are 32 bits and wrap around at 0xFFFFFFFF, - * CLOG page numbering also wraps around at 0xFFFFFFFF/CLOG_XACTS_PER_PAGE, - * and CLOG segment numbering at - * 0xFFFFFFFF/CLOG_XACTS_PER_PAGE/SLRU_PAGES_PER_SEGMENT. We need take no - * explicit notice of that fact in this module, except when comparing segment - * and page numbers in TruncateCLOG (see CLOGPagePrecedes). - */ - -/* We need two bits per xact, so four xacts fit in a byte */ -#define CLOG_BITS_PER_XACT 2 -#define CLOG_XACTS_PER_BYTE 4 -#define CLOG_XACTS_PER_PAGE (BLCKSZ * CLOG_XACTS_PER_BYTE) -#define CLOG_XACT_BITMASK ((1 << CLOG_BITS_PER_XACT) - 1) - -#define TransactionIdToPage(xid) ((xid) / (TransactionId) CLOG_XACTS_PER_PAGE) -#define TransactionIdToPgIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_PAGE) -#define TransactionIdToByte(xid) (TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE) -#define TransactionIdToBIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_BYTE) - /* We store the latest async LSN for each group of transactions */ #define CLOG_XACTS_PER_LSN_GROUP 32 /* keep this a power of 2 */ #define CLOG_LSNS_PER_PAGE (CLOG_XACTS_PER_PAGE / CLOG_XACTS_PER_LSN_GROUP) diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 4fc5d5a..4194195 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1008,6 +1008,26 @@ LANGUAGE INTERNAL STRICT IMMUTABLE PARALLEL SAFE AS 'jsonb_insert'; +CREATE TYPE txid_status AS ENUM ('committed', 'in-progress', 'aborted'); + +CREATE FUNCTION + txid_status(txid bigint) +RETURNS txid_status +LANGUAGE sql +VOLATILE PARALLEL SAFE +AS $$ +SELECT CASE + WHEN s IS NULL THEN NULL::txid_status + WHEN s = -1 THEN 'aborted'::txid_status + WHEN s = 0 THEN 'in-progress'::txid_status + WHEN s = 1 THEN 'committed'::txid_status +END +FROM pg_catalog.txid_status_internal($1) s; +$$; + +COMMENT ON FUNCTION txid_status(bigint) +IS 'get commit status of given recent xid or null if too old'; + -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather -- than use explicit 'superuser()' checks in those functions, we use the GRANT diff --git a/src/backend/utils/adt/txid.c b/src/backend/utils/adt/txid.c index c2069a9..0480c99 100644 --- a/src/backend/utils/adt/txid.c +++ b/src/backend/utils/adt/txid.c @@ -21,9 +21,11 @@ #include "postgres.h" +#include "access/clog.h" #include "access/transam.h" #include "access/xact.h" #include "access/xlog.h" +#include "catalog/pg_proc.h" #include "funcapi.h" #include "miscadmin.h" #include "libpq/pqformat.h" @@ -117,6 +119,50 @@ convert_xid(TransactionId xid, const TxidEpoch *state) } /* + * Helper to get a TransactionId from a 64-bit txid with wraparound detection. + * ERRORs if the txid is in the future. Returns permanent XIDs unchanged. + * Otherwise returns the 32-bit xid and sets the wraparound param to true + * if wraparound is detected, false otherwise. + */ +static TransactionId +get_xid_in_recent_past(txid xid_with_epoch, bool *wraparound) +{ + uint32 xid_epoch = (uint32)(xid_with_epoch >>32); + TransactionId xid = (TransactionId)(xid_with_epoch); + TxidEpoch now_epoch; + + load_xid_epoch(&now_epoch); + + *wraparound = false; + + if (!TransactionIdIsNormal(xid)) + { + /* must be a permanent XID, ignore the epoch and return unchanged */ + return xid; + } + else if (xid_epoch > now_epoch.epoch + || (xid_epoch == now_epoch.epoch && xid > now_epoch.last_xid)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("transaction ID "UINT64_FORMAT" is in the future", + xid_with_epoch))); + } + else if (xid_epoch + 1 < now_epoch.epoch + || (xid_epoch + 1 == now_epoch.epoch && xid < now_epoch.last_xid)) + { + /* xid too far in the past */ + *wraparound = true; + } + else + { + Assert(TransactionIdPrecedesOrEquals(xid, now_epoch.last_xid)); + } + + return xid; +} + +/* * txid comparator for qsort/bsearch */ static int @@ -354,6 +400,9 @@ bad_format: * * Return the current toplevel transaction ID as TXID * If the current transaction does not have one, one is assigned. + * + * This value has the epoch as the high 32 bits and the 32-bit xid + * as the low 32 bits. */ Datum txid_current(PG_FUNCTION_ARGS) @@ -637,3 +686,36 @@ txid_snapshot_xip(PG_FUNCTION_ARGS) SRF_RETURN_DONE(fctx); } } + +/* + * Underlying implementation of txid_status, which is mapped to an enum in + * system_views.sql. + */ +Datum +txid_status_internal(PG_FUNCTION_ARGS) +{ + bool wraparound; + uint64 xid_with_epoch = PG_GETARG_INT64(0); + TransactionId xid = get_xid_in_recent_past(xid_with_epoch, &wraparound); + + if (!TransactionIdIsValid(xid)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("transaction ID "UINT64_FORMAT" is an invalid xid", + xid_with_epoch))); + } + + if (wraparound) + PG_RETURN_NULL(); + else if (TransactionIdIsCurrentTransactionId(xid) || TransactionIdIsInProgress(xid)) + PG_RETURN_INT32(0); + else if (TransactionIdDidCommit(xid)) + PG_RETURN_INT32(1); + else if (TransactionIdDidAbort(xid)) + PG_RETURN_INT32(-1); + else + /* shouldn't happen */ + ereport(ERROR, + (errmsg_internal("unable to determine commit status of xid "UINT64_FORMAT, xid))); +} diff --git a/src/include/access/clog.h b/src/include/access/clog.h index 06c069a..a763dfb 100644 --- a/src/include/access/clog.h +++ b/src/include/access/clog.h @@ -28,6 +28,29 @@ typedef int XidStatus; #define TRANSACTION_STATUS_ABORTED 0x02 #define TRANSACTION_STATUS_SUB_COMMITTED 0x03 +/* + * Defines for CLOG page sizes. A page is the same BLCKSZ as is used + * everywhere else in Postgres. + * + * Note: because TransactionIds are 32 bits and wrap around at 0xFFFFFFFF, + * CLOG page numbering also wraps around at 0xFFFFFFFF/CLOG_XACTS_PER_PAGE, + * and CLOG segment numbering at + * 0xFFFFFFFF/CLOG_XACTS_PER_PAGE/SLRU_PAGES_PER_SEGMENT. We need take no + * explicit notice of that fact in this module, except when comparing segment + * and page numbers in TruncateCLOG (see CLOGPagePrecedes). + */ + +/* We need two bits per xact, so four xacts fit in a byte */ +#define CLOG_BITS_PER_XACT 2 +#define CLOG_XACTS_PER_BYTE 4 +#define CLOG_XACTS_PER_PAGE (BLCKSZ * CLOG_XACTS_PER_BYTE) +#define CLOG_XACT_BITMASK ((1 << CLOG_BITS_PER_XACT) - 1) + +#define TransactionIdToPage(xid) ((xid) / (TransactionId) CLOG_XACTS_PER_PAGE) +#define TransactionIdToPgIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_PAGE) +#define TransactionIdToByte(xid) (TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE) +#define TransactionIdToBIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_BYTE) + extern void TransactionIdSetTreeStatus(TransactionId xid, int nsubxids, TransactionId *subxids, XidStatus status, XLogRecPtr lsn); diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index 270dd21..b0b25e6 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -4903,6 +4903,8 @@ DATA(insert OID = 2947 ( txid_snapshot_xip PGNSP PGUID 12 1 50 0 0 f f f f t DESCR("get set of in-progress txids in snapshot"); DATA(insert OID = 2948 ( txid_visible_in_snapshot PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "20 2970" _null_ _null_ _null_ _null_ _null_ txid_visible_in_snapshot _null_ _null_ _null_ )); DESCR("is txid visible in snapshot?"); +DATA(insert OID = 3346 ( txid_status_internal PGNSP PGUID 12 1 0 0 0 f f f f t f v s 1 0 23 "20" _null_ _null_ _null_ _null_ _null_ txid_status_internal _null_ _null_ _null_ )); +DESCR("commit status of transaction"); /* record comparison using normal comparison rules */ DATA(insert OID = 2981 ( record_eq PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ _null_ record_eq _null_ _null_ _null_ )); diff --git a/src/test/regress/expected/txid.out b/src/test/regress/expected/txid.out index ddd217e..61299c5 100644 --- a/src/test/regress/expected/txid.out +++ b/src/test/regress/expected/txid.out @@ -238,3 +238,53 @@ SELECT txid_snapshot '1:9223372036854775808:3'; ERROR: invalid input syntax for type txid_snapshot: "1:9223372036854775808:3" LINE 1: SELECT txid_snapshot '1:9223372036854775808:3'; ^ +-- test xid status functions +BEGIN; +SELECT txid_current() AS committed \gset +COMMIT; +BEGIN; +SELECT txid_current() AS rolledback \gset +ROLLBACK; +BEGIN; +SELECT txid_current() AS inprogress \gset +SELECT txid_status(:committed) AS committed; + committed +----------- + committed +(1 row) + +SELECT txid_status(:rolledback) AS rolledback; + rolledback +------------ + aborted +(1 row) + +SELECT txid_status(:inprogress) AS inprogress; + inprogress +------------- + in-progress +(1 row) + +COMMIT; +BEGIN; +CREATE FUNCTION test_future_xid_status(bigint) +RETURNS void +LANGUAGE plpgsql +AS +$$ +BEGIN + PERFORM txid_status($1); + RAISE EXCEPTION 'didn''t ERROR at xid in the future as expected'; +EXCEPTION + WHEN invalid_parameter_value THEN + RAISE NOTICE 'Got expected error for xid in the future'; +END; +$$; +SELECT test_future_xid_status(:inprogress + 10000); +NOTICE: Got expected error for xid in the future + test_future_xid_status +------------------------ + +(1 row) + +ROLLBACK; diff --git a/src/test/regress/sql/txid.sql b/src/test/regress/sql/txid.sql index b6650b9..50d4461 100644 --- a/src/test/regress/sql/txid.sql +++ b/src/test/regress/sql/txid.sql @@ -52,3 +52,39 @@ select txid_visible_in_snapshot('1000100010001015', '1000100010001000:1000100010 -- test 64bit overflow SELECT txid_snapshot '1:9223372036854775807:3'; SELECT txid_snapshot '1:9223372036854775808:3'; + +-- test xid status functions +BEGIN; +SELECT txid_current() AS committed \gset +COMMIT; + +BEGIN; +SELECT txid_current() AS rolledback \gset +ROLLBACK; + +BEGIN; +SELECT txid_current() AS inprogress \gset + +SELECT txid_status(:committed) AS committed; +SELECT txid_status(:rolledback) AS rolledback; +SELECT txid_status(:inprogress) AS inprogress; + +COMMIT; + +BEGIN; +CREATE FUNCTION test_future_xid_status(bigint) +RETURNS void +LANGUAGE plpgsql +AS +$$ +BEGIN + PERFORM txid_status($1); + RAISE EXCEPTION 'didn''t ERROR at xid in the future as expected'; +EXCEPTION + WHEN invalid_parameter_value THEN + RAISE NOTICE 'Got expected error for xid in the future'; +END; +$$; +SELECT test_future_xid_status(:inprogress + 10000); +ROLLBACK; + -- 2.5.5
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers