mirror of
https://github.com/postgres/postgres.git
synced 2025-06-13 07:41:39 +03:00
Add support for prepared transactions to built-in logical replication.
To add support for streaming transactions at prepare time into the built-in logical replication, we need to do the following things: * Modify the output plugin (pgoutput) to implement the new two-phase API callbacks, by leveraging the extended replication protocol. * Modify the replication apply worker, to properly handle two-phase transactions by replaying them on prepare. * Add a new SUBSCRIPTION option "two_phase" to allow users to enable two-phase transactions. We enable the two_phase once the initial data sync is over. We however must explicitly disable replication of two-phase transactions during replication slot creation, even if the plugin supports it. We don't need to replicate the changes accumulated during this phase, and moreover, we don't have a replication connection open so we don't know where to send the data anyway. The streaming option is not allowed with this new two_phase option. This can be done as a separate patch. We don't allow to toggle two_phase option of a subscription because it can lead to an inconsistent replica. For the same reason, we don't allow to refresh the publication once the two_phase is enabled for a subscription unless copy_data option is false. Author: Peter Smith, Ajin Cherian and Amit Kapila based on previous work by Nikhil Sontakke and Stas Kelvich Reviewed-by: Amit Kapila, Sawada Masahiko, Vignesh C, Dilip Kumar, Takamichi Osumi, Greg Nancarrow Tested-By: Haiying Tang Discussion: https://postgr.es/m/02DA5F5E-CECE-4D9C-8B4B-418077E2C010@postgrespro.ru Discussion: https://postgr.es/m/CAA4eK1+opiV4aFTmWWUF9h_32=HfPOW9vZASHarT0UA5oBrtGw@mail.gmail.com
This commit is contained in:
@ -59,6 +59,7 @@
|
||||
#define SUBOPT_REFRESH 0x00000040
|
||||
#define SUBOPT_BINARY 0x00000080
|
||||
#define SUBOPT_STREAMING 0x00000100
|
||||
#define SUBOPT_TWOPHASE_COMMIT 0x00000200
|
||||
|
||||
/* check if the 'val' has 'bits' set */
|
||||
#define IsSet(val, bits) (((val) & (bits)) == (bits))
|
||||
@ -79,6 +80,7 @@ typedef struct SubOpts
|
||||
bool refresh;
|
||||
bool binary;
|
||||
bool streaming;
|
||||
bool twophase;
|
||||
} SubOpts;
|
||||
|
||||
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
|
||||
@ -123,6 +125,8 @@ parse_subscription_options(List *stmt_options, bits32 supported_opts, SubOpts *o
|
||||
opts->binary = false;
|
||||
if (IsSet(supported_opts, SUBOPT_STREAMING))
|
||||
opts->streaming = false;
|
||||
if (IsSet(supported_opts, SUBOPT_TWOPHASE_COMMIT))
|
||||
opts->twophase = false;
|
||||
|
||||
/* Parse options */
|
||||
foreach(lc, stmt_options)
|
||||
@ -237,6 +241,29 @@ parse_subscription_options(List *stmt_options, bits32 supported_opts, SubOpts *o
|
||||
opts->specified_opts |= SUBOPT_STREAMING;
|
||||
opts->streaming = defGetBoolean(defel);
|
||||
}
|
||||
else if (strcmp(defel->defname, "two_phase") == 0)
|
||||
{
|
||||
/*
|
||||
* Do not allow toggling of two_phase option. Doing so could cause
|
||||
* missing of transactions and lead to an inconsistent replica.
|
||||
* See comments atop worker.c
|
||||
*
|
||||
* Note: Unsupported twophase indicates that this call originated
|
||||
* from AlterSubscription.
|
||||
*/
|
||||
if (!IsSet(supported_opts, SUBOPT_TWOPHASE_COMMIT))
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("unrecognized subscription parameter: \"%s\"", defel->defname)));
|
||||
|
||||
if (IsSet(opts->specified_opts, SUBOPT_TWOPHASE_COMMIT))
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("conflicting or redundant options")));
|
||||
|
||||
opts->specified_opts |= SUBOPT_TWOPHASE_COMMIT;
|
||||
opts->twophase = defGetBoolean(defel);
|
||||
}
|
||||
else
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
@ -325,6 +352,25 @@ parse_subscription_options(List *stmt_options, bits32 supported_opts, SubOpts *o
|
||||
errmsg("subscription with %s must also set %s",
|
||||
"slot_name = NONE", "create_slot = false")));
|
||||
}
|
||||
|
||||
/*
|
||||
* Do additional checking for the disallowed combination of two_phase and
|
||||
* streaming. While streaming and two_phase can theoretically be
|
||||
* supported, it needs more analysis to allow them together.
|
||||
*/
|
||||
if (opts->twophase &&
|
||||
IsSet(supported_opts, SUBOPT_TWOPHASE_COMMIT) &&
|
||||
IsSet(opts->specified_opts, SUBOPT_TWOPHASE_COMMIT))
|
||||
{
|
||||
if (opts->streaming &&
|
||||
IsSet(supported_opts, SUBOPT_STREAMING) &&
|
||||
IsSet(opts->specified_opts, SUBOPT_STREAMING))
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
/*- translator: both %s are strings of the form "option = value" */
|
||||
errmsg("%s and %s are mutually exclusive options",
|
||||
"two_phase = true", "streaming = true")));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -385,7 +431,7 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
|
||||
supported_opts = (SUBOPT_CONNECT | SUBOPT_ENABLED | SUBOPT_CREATE_SLOT |
|
||||
SUBOPT_SLOT_NAME | SUBOPT_COPY_DATA |
|
||||
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
|
||||
SUBOPT_STREAMING);
|
||||
SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT);
|
||||
parse_subscription_options(stmt->options, supported_opts, &opts);
|
||||
|
||||
/*
|
||||
@ -455,6 +501,10 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
|
||||
values[Anum_pg_subscription_subenabled - 1] = BoolGetDatum(opts.enabled);
|
||||
values[Anum_pg_subscription_subbinary - 1] = BoolGetDatum(opts.binary);
|
||||
values[Anum_pg_subscription_substream - 1] = BoolGetDatum(opts.streaming);
|
||||
values[Anum_pg_subscription_subtwophasestate - 1] =
|
||||
CharGetDatum(opts.twophase ?
|
||||
LOGICALREP_TWOPHASE_STATE_PENDING :
|
||||
LOGICALREP_TWOPHASE_STATE_DISABLED);
|
||||
values[Anum_pg_subscription_subconninfo - 1] =
|
||||
CStringGetTextDatum(conninfo);
|
||||
if (opts.slot_name)
|
||||
@ -532,10 +582,35 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
|
||||
*/
|
||||
if (opts.create_slot)
|
||||
{
|
||||
bool twophase_enabled = false;
|
||||
|
||||
Assert(opts.slot_name);
|
||||
|
||||
walrcv_create_slot(wrconn, opts.slot_name, false,
|
||||
/*
|
||||
* Even if two_phase is set, don't create the slot with
|
||||
* two-phase enabled. Will enable it once all the tables are
|
||||
* synced and ready. This avoids race-conditions like prepared
|
||||
* transactions being skipped due to changes not being applied
|
||||
* due to checks in should_apply_changes_for_rel() when
|
||||
* tablesync for the corresponding tables are in progress. See
|
||||
* comments atop worker.c.
|
||||
*
|
||||
* Note that if tables were specified but copy_data is false
|
||||
* then it is safe to enable two_phase up-front because those
|
||||
* tables are already initially in READY state. When the
|
||||
* subscription has no tables, we leave the twophase state as
|
||||
* PENDING, to allow ALTER SUBSCRIPTION ... REFRESH
|
||||
* PUBLICATION to work.
|
||||
*/
|
||||
if (opts.twophase && !opts.copy_data && tables != NIL)
|
||||
twophase_enabled = true;
|
||||
|
||||
walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
|
||||
CRS_NOEXPORT_SNAPSHOT, NULL);
|
||||
|
||||
if (twophase_enabled)
|
||||
UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
|
||||
|
||||
ereport(NOTICE,
|
||||
(errmsg("created replication slot \"%s\" on publisher",
|
||||
opts.slot_name)));
|
||||
@ -865,6 +940,12 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
|
||||
|
||||
if (IsSet(opts.specified_opts, SUBOPT_STREAMING))
|
||||
{
|
||||
if ((sub->twophasestate != LOGICALREP_TWOPHASE_STATE_DISABLED) && opts.streaming)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("cannot set %s for two-phase enabled subscription",
|
||||
"streaming = true")));
|
||||
|
||||
values[Anum_pg_subscription_substream - 1] =
|
||||
BoolGetDatum(opts.streaming);
|
||||
replaces[Anum_pg_subscription_substream - 1] = true;
|
||||
@ -927,6 +1008,17 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
|
||||
errmsg("ALTER SUBSCRIPTION with refresh is not allowed for disabled subscriptions"),
|
||||
errhint("Use ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = false).")));
|
||||
|
||||
/*
|
||||
* See ALTER_SUBSCRIPTION_REFRESH for details why this is
|
||||
* not allowed.
|
||||
*/
|
||||
if (sub->twophasestate == LOGICALREP_TWOPHASE_STATE_ENABLED && opts.copy_data)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("ALTER SUBSCRIPTION with refresh and copy_data is not allowed when two_phase is enabled"),
|
||||
errhint("Use ALTER SUBSCRIPTION ...SET PUBLICATION with refresh = false, or with copy_data = false"
|
||||
", or use DROP/CREATE SUBSCRIPTION.")));
|
||||
|
||||
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION with refresh");
|
||||
|
||||
/* Make sure refresh sees the new list of publications. */
|
||||
@ -966,6 +1058,17 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
|
||||
errmsg("ALTER SUBSCRIPTION with refresh is not allowed for disabled subscriptions"),
|
||||
errhint("Use ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = false).")));
|
||||
|
||||
/*
|
||||
* See ALTER_SUBSCRIPTION_REFRESH for details why this is
|
||||
* not allowed.
|
||||
*/
|
||||
if (sub->twophasestate == LOGICALREP_TWOPHASE_STATE_ENABLED && opts.copy_data)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("ALTER SUBSCRIPTION with refresh and copy_data is not allowed when two_phase is enabled"),
|
||||
errhint("Use ALTER SUBSCRIPTION ...SET PUBLICATION with refresh = false, or with copy_data = false"
|
||||
", or use DROP/CREATE SUBSCRIPTION.")));
|
||||
|
||||
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION with refresh");
|
||||
|
||||
/* Only refresh the added/dropped list of publications. */
|
||||
@ -986,6 +1089,30 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
|
||||
|
||||
parse_subscription_options(stmt->options, SUBOPT_COPY_DATA, &opts);
|
||||
|
||||
/*
|
||||
* The subscription option "two_phase" requires that
|
||||
* replication has passed the initial table synchronization
|
||||
* phase before the two_phase becomes properly enabled.
|
||||
*
|
||||
* But, having reached this two-phase commit "enabled" state
|
||||
* we must not allow any subsequent table initialization to
|
||||
* occur. So the ALTER SUBSCRIPTION ... REFRESH is disallowed
|
||||
* when the user had requested two_phase = on mode.
|
||||
*
|
||||
* The exception to this restriction is when copy_data =
|
||||
* false, because when copy_data is false the tablesync will
|
||||
* start already in READY state and will exit directly without
|
||||
* doing anything.
|
||||
*
|
||||
* For more details see comments atop worker.c.
|
||||
*/
|
||||
if (sub->twophasestate == LOGICALREP_TWOPHASE_STATE_ENABLED && opts.copy_data)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("ALTER SUBSCRIPTION ... REFRESH with copy_data is not allowed when two_phase is enabled"),
|
||||
errhint("Use ALTER SUBSCRIPTION ... REFRESH with copy_data = false"
|
||||
", or use DROP/CREATE SUBSCRIPTION.")));
|
||||
|
||||
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
|
||||
|
||||
AlterSubscription_refresh(sub, opts.copy_data);
|
||||
|
Reference in New Issue
Block a user