From 37e3f6195a7ba58de0706f00bfec630c85d3d530 Mon Sep 17 00:00:00 2001 From: Craig Ringer Date: Fri, 19 Aug 2016 14:44:15 +0800 Subject: [PATCH 2/4] 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. To handle failure to access the clog when the xid specified is newer than the wraparound threshold but older than the clog truncation threshold we need to catch exceptions and check to see if the failure was an I/O error. The only documented way to get the error code is for use in errcontext callbacks, the errcode() call. There's no good reason it can't also be used in PG_CATCH() blocks so amend the comment in elog.c. --- doc/src/sgml/func.sgml | 26 ++++++++ src/backend/access/transam/clog.c | 23 ------- src/backend/utils/adt/txid.c | 132 +++++++++++++++++++++++++++++++++++++ src/backend/utils/error/elog.c | 5 +- src/include/access/clog.h | 23 +++++++ src/include/catalog/pg_proc.h | 2 + src/include/utils/builtins.h | 1 + src/test/regress/expected/txid.out | 68 +++++++++++++++++++ src/test/regress/sql/txid.sql | 38 +++++++++++ 9 files changed, 293 insertions(+), 25 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 5148095..db11f29 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17143,6 +17143,10 @@ SELECT collation for ('foo' COLLATE "de_DE"); txid_visible_in_snapshot + + txid_status + + The functions shown in provide server transaction information in an exportable form. The main @@ -17193,6 +17197,11 @@ SELECT collation for ('foo' COLLATE "de_DE"); boolean is transaction ID visible in snapshot? (do not use with subtransaction ids) + + txid_status(bigint) + txid_status + report the status of the given xact - committed, aborted, in progress, or NULL if the xid is too old + @@ -17263,6 +17272,23 @@ SELECT collation for ('foo' COLLATE "de_DE"); + txid_status(bigint) reports the commit status of a recent + transaction. Any recent transaction can be identified as one of + + in progress + committed + aborted + + Prepared transactions are identified as in progress. + The commit status of transactions older than the transaction ID wrap-around + threshold is no longer known by the system, so txid_status + returns NULL for such transactions. Applications may use + txid_status to determine whether a transaction committed + or aborted when the application and/or database server crashed or lost + connection while a COMMIT command was in progress. + + + The functions shown in provide information about transactions that have been already committed. These functions mainly provide information about when the transactions 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/utils/adt/txid.c b/src/backend/utils/adt/txid.c index 276075e..a0214a5 100644 --- a/src/backend/utils/adt/txid.c +++ b/src/backend/utils/adt/txid.c @@ -21,6 +21,7 @@ #include "postgres.h" +#include "access/clog.h" #include "access/transam.h" #include "access/xact.h" #include "access/xlog.h" @@ -117,6 +118,64 @@ 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 + * too_old param to true if wraparound for this xid is close, + * otherwise false. + * + * To guard against wraparound occurring just after we check the xid, + * such that the xid is not wrapped when we return it but wraps + * shortly afterwards, treat xids more than 2^30 from the current + * nextXid as too old. That way there's a large margin of nextXid + * growth before TransactionIdPrecedes would start reporting the + * queried xid as in the future relative to new xacts as it crosses + * the 2^31 difference boundary. + * + * In practice the clog will usually be truncated away for such xids + * already so they're of limited utility. + */ +static TransactionId +get_xid_in_recent_past(txid xid_with_epoch, bool *too_old) +{ + uint32 xid_epoch = (uint32) (xid_with_epoch >> 32); + TransactionId xid = (TransactionId) xid_with_epoch; + TxidEpoch now_epoch; + const int wraparound_safety_margin = 1 << 30; + + load_xid_epoch(&now_epoch); + + *too_old = 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 + && (now_epoch.last_xid - xid) > wraparound_safety_margin)) + { + /* xid too far in the past */ + *too_old = true; + } + else + { + Assert(TransactionIdPrecedesOrEquals(xid, now_epoch.last_xid)); + } + + return xid; +} + +/* * txid comparator for qsort/bsearch */ static int @@ -354,6 +413,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) @@ -658,3 +720,73 @@ txid_snapshot_xip(PG_FUNCTION_ARGS) SRF_RETURN_DONE(fctx); } } + +/* + * Report the status of a recent transaction ID, or null for wrapped, + * truncated away or otherwise too old XIDs. + */ +Datum +txid_status(PG_FUNCTION_ARGS) +{ + const char *status; + bool too_old; + uint64 xid_with_epoch = PG_GETARG_INT64(0); + TransactionId xid = get_xid_in_recent_past(xid_with_epoch, &too_old); + + if (!TransactionIdIsValid(xid)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("transaction ID "UINT64_FORMAT" is an invalid xid", + xid_with_epoch))); + } + + if (too_old) + status = NULL; + else if (TransactionIdIsCurrentTransactionId(xid)) + status = gettext_noop("in progress"); + else + { + /* + * clog access might fail here if the requested xid is past the + * clog truncation threshold but not wrapped around. We want to + * return null in such cases rather than raise an error. + */ + PG_TRY(); + { + if (TransactionIdDidCommit(xid)) + status = gettext_noop("committed"); + else if (TransactionIdDidAbort(xid)) + status = gettext_noop("aborted"); + else + /* + * can't test TransactionIdIsInProgress here or we race + * with concurrent commit/abort. There's no point anyway, + * since it might then commit/abort just after we check. + */ + status = gettext_noop("in progress"); + } + PG_CATCH(); + { + if (geterrcode() == ERRCODE_UNDEFINED_FILE) + { + /* + * The clog SLRU reports that it can't find the segment for + * the xid of interest. Presumably it has been truncated + * away, so the xact is too old to report status for. + */ + elog(INFO, "clog access failed with access error"); + status = NULL; + } + else + PG_RE_THROW(); + + } + PG_END_TRY(); + } + + if (status == NULL) + PG_RETURN_NULL(); + else + PG_RETURN_TEXT_P(cstring_to_text(status)); +} diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index 224ee78..10bca81 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -1233,8 +1233,9 @@ set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str) /* * geterrcode --- return the currently set SQLSTATE error code * - * This is only intended for use in error callback subroutines, since there - * is no other place outside elog.c where the concept is meaningful. + * This is only intended for use in error callback subroutines and PG_CATCH + * blocks, since there is no other place outside elog.c where the concept is + * meaningful. */ int geterrcode(void) 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 e2d08ba..0ad870c 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -4928,6 +4928,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 PGNSP PGUID 12 1 0 0 0 f f f f t f v s 1 0 25 "20" _null_ _null_ _null_ _null_ _null_ txid_status _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/include/utils/builtins.h b/src/include/utils/builtins.h index 2ae212a..baffa38 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -1227,6 +1227,7 @@ extern Datum txid_snapshot_xmin(PG_FUNCTION_ARGS); extern Datum txid_snapshot_xmax(PG_FUNCTION_ARGS); extern Datum txid_snapshot_xip(PG_FUNCTION_ARGS); extern Datum txid_visible_in_snapshot(PG_FUNCTION_ARGS); +extern Datum txid_status(PG_FUNCTION_ARGS); /* uuid.c */ extern Datum uuid_in(PG_FUNCTION_ARGS); diff --git a/src/test/regress/expected/txid.out b/src/test/regress/expected/txid.out index 802ccb9..1a0b84c 100644 --- a/src/test/regress/expected/txid.out +++ b/src/test/regress/expected/txid.out @@ -254,3 +254,71 @@ SELECT txid_current_if_assigned() IS NOT DISTINCT FROM BIGINT :'txid_current'; (1 row) COMMIT; +-- 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) + +SELECT txid_status(1); -- BootstrapTransactionId is always committed + txid_status +------------- + committed +(1 row) + +SELECT txid_status(2); -- FrozenTransactionId is always committed + txid_status +------------- + committed +(1 row) + +SELECT txid_status(3); -- FirstNormalTransactionId is always committed for the first epoch because of initdb work + txid_status +------------- + committed +(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 4aefd9e..1c56ec1 100644 --- a/src/test/regress/sql/txid.sql +++ b/src/test/regress/sql/txid.sql @@ -59,3 +59,41 @@ SELECT txid_current_if_assigned() IS NULL; SELECT txid_current() \gset SELECT txid_current_if_assigned() IS NOT DISTINCT FROM BIGINT :'txid_current'; COMMIT; + +-- 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; +SELECT txid_status(1); -- BootstrapTransactionId is always committed +SELECT txid_status(2); -- FrozenTransactionId is always committed +SELECT txid_status(3); -- FirstNormalTransactionId is always committed for the first epoch because of initdb work + +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