From f5319845a26b07ce988736516a474d909a9a168a Mon Sep 17 00:00:00 2001 From: "houzj.fnst" Date: Wed, 20 Apr 2022 16:45:07 +0800 Subject: [PATCH v9 1/4] Perform streaming logical transactions by background workers Currently, for large transactions, the publisher sends the data in multiple streams (changes divided into chunks depending upon logical_decoding_work_mem), and then on the subscriber-side, the apply worker writes the changes into temporary files and once it receives the commit, it reads from the file and applies the entire transaction. To improve the performance of such transactions, we can instead allow them to be applied via background workers. In this approach, we assign a new apply background worker (if available) as soon as the xact's first stream is received and the main apply worker will send changes to this new worker via shared memory. The apply background worker will directly apply the change instead of writing it to temporary files. We keep this worker assigned till the transaction commit is received and also wait for the worker to finish at commit. This preserves commit ordering and avoids writing to and reading from file in most cases. We still need to spill if there is no worker available. We also need to allow stream_stop to complete by the apply background worker to avoid deadlocks because T-1's current stream of changes can update rows in conflicting order with T-2's next stream of changes. This patch also extends the SUBSCRIPTION 'streaming' option so that the user can control whether to apply the streaming transaction in an apply background worker or spill the change to disk. User can set the streaming option to 'on/off', 'apply'. For now, 'apply' means the streaming will be applied via a apply background worker if available. 'on' means the streaming transaction will be spilled to disk. --- doc/src/sgml/catalogs.sgml | 10 +- doc/src/sgml/logical-replication.sgml | 7 + doc/src/sgml/protocol.sgml | 19 + doc/src/sgml/ref/create_subscription.sgml | 24 +- src/backend/access/transam/xact.c | 13 + src/backend/commands/subscriptioncmds.c | 66 +- src/backend/postmaster/bgworker.c | 3 + src/backend/replication/logical/Makefile | 3 +- .../replication/logical/applybgwroker.c | 756 ++++++++++++++++++ src/backend/replication/logical/decode.c | 10 +- src/backend/replication/logical/launcher.c | 87 +- src/backend/replication/logical/origin.c | 26 +- src/backend/replication/logical/proto.c | 35 +- .../replication/logical/reorderbuffer.c | 10 +- src/backend/replication/logical/tablesync.c | 10 +- src/backend/replication/logical/worker.c | 645 +++++++++++---- src/backend/replication/pgoutput/pgoutput.c | 2 +- src/backend/utils/activity/wait_event.c | 3 + src/bin/pg_dump/pg_dump.c | 6 +- src/include/catalog/pg_subscription.h | 17 +- src/include/replication/logicalproto.h | 19 +- src/include/replication/logicalworker.h | 1 + src/include/replication/origin.h | 2 +- src/include/replication/reorderbuffer.h | 7 +- src/include/replication/worker_internal.h | 103 ++- src/include/utils/wait_event.h | 1 + src/test/regress/expected/subscription.out | 2 +- src/tools/pgindent/typedefs.list | 5 + 28 files changed, 1677 insertions(+), 215 deletions(-) create mode 100644 src/backend/replication/logical/applybgwroker.c diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index c00c93dd7b..5cb39b18bd 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -7873,11 +7873,15 @@ SCRAM-SHA-256$<iteration count>:&l - substream bool + substream char - If true, the subscription will allow streaming of in-progress - transactions + Controls how to handle the streaming of in-progress transactions: + f = disallow streaming of in-progress transactions, + t = spill the changes of in-progress transactions to + disk and apply at once after the transaction is committed on the + publisher, + a = apply changes directly using a background worker diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index 145ea71d61..cca6c97d79 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -948,6 +948,13 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER might not violate any constraint. This can easily make the subscriber inconsistent. + + + Setting streaming mode to apply could export invalid LSN + as finish LSN of failed transaction. Changing the streaming mode and making + the same conflict writes the finish LSN of the failed transaction in the + server log if required. + diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index bb30340892..34be1b2698 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -6809,6 +6809,25 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" + + Int64 (XLogRecPtr) + + + The LSN of the abort. + + + + + + Int64 (TimestampTz) + + + Abort timestamp of the transaction. The value is in number + of microseconds since PostgreSQL epoch (2000-01-01). + + + + Int32 (TransactionId) diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml index 35b39c28da..d4caae0222 100644 --- a/doc/src/sgml/ref/create_subscription.sgml +++ b/doc/src/sgml/ref/create_subscription.sgml @@ -217,13 +217,29 @@ CREATE SUBSCRIPTION subscription_name - streaming (boolean) + streaming (enum) Specifies whether to enable streaming of in-progress transactions - for this subscription. By default, all transactions - are fully decoded on the publisher and only then sent to the - subscriber as a whole. + for this subscription. The default value is off, + all transactions are fully decoded on the publisher and only then + sent to the subscriber as a whole. + + + + If set to on, the changes of transaction are + written to temporary files and then applied at once after the + transaction is committed on the publisher. + + + + If set to apply incoming + changes are directly applied via one of the background workers, if + available. If no background worker is free to handle streaming + transaction then the changes are written to a file and applied after + the transaction is committed. Note that if an error happens when + applying changes in a background worker, it might not report the + finish LSN of the remote transaction in the server log. diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 47d80b0d25..678540ba25 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -1711,6 +1711,7 @@ RecordTransactionAbort(bool isSubXact) int nchildren; TransactionId *children; TimestampTz xact_time; + bool replorigin; /* * If we haven't been assigned an XID, nobody will care whether we aborted @@ -1741,6 +1742,13 @@ RecordTransactionAbort(bool isSubXact) elog(PANIC, "cannot abort transaction %u, it was already committed", xid); + /* + * Are we using the replication origins feature? Or, in other words, + * are we replaying remote actions? + */ + replorigin = (replorigin_session_origin != InvalidRepOriginId && + replorigin_session_origin != DoNotReplicateId); + /* Fetch the data we need for the abort record */ nrels = smgrGetPendingDeletes(false, &rels); nchildren = xactGetCommittedChildren(&children); @@ -1765,6 +1773,11 @@ RecordTransactionAbort(bool isSubXact) MyXactFlags, InvalidTransactionId, NULL); + if (replorigin) + /* Move LSNs forward for this replication origin */ + replorigin_session_advance(replorigin_session_origin_lsn, + XactLastRecEnd); + /* * Report the latest async abort LSN, so that the WAL writer knows to * flush this abort. There's nothing to be gained by delaying this, since diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 83e6eae855..4080bba987 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -83,7 +83,7 @@ typedef struct SubOpts bool copy_data; bool refresh; bool binary; - bool streaming; + char streaming; bool twophase; bool disableonerr; XLogRecPtr lsn; @@ -95,6 +95,62 @@ static List *merge_publications(List *oldpublist, List *newpublist, bool addpub, static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err); +/* + * Extract the streaming mode value from a DefElem. This is like + * defGetBoolean() but also accepts the special value of "apply". + */ +static char +defGetStreamingMode(DefElem *def) +{ + /* + * If no parameter given, assume "true" is meant. + */ + if (def->arg == NULL) + return SUBSTREAM_ON; + + /* + * Allow 0, 1, "true", "false", "on", "off" or "apply". + */ + switch (nodeTag(def->arg)) + { + case T_Integer: + switch (intVal(def->arg)) + { + case 0: + return SUBSTREAM_OFF; + case 1: + return SUBSTREAM_ON; + default: + /* otherwise, error out below */ + break; + } + break; + default: + { + char *sval = defGetString(def); + + /* + * The set of strings accepted here should match up with the + * grammar's opt_boolean_or_string production. + */ + if (pg_strcasecmp(sval, "false") == 0 || + pg_strcasecmp(sval, "off") == 0) + return SUBSTREAM_OFF; + if (pg_strcasecmp(sval, "true") == 0 || + pg_strcasecmp(sval, "on") == 0) + return SUBSTREAM_ON; + if (pg_strcasecmp(sval, "apply") == 0) + return SUBSTREAM_APPLY; + } + break; + } + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("%s requires a Boolean value or \"apply\"", + def->defname))); + return SUBSTREAM_OFF; /* keep compiler quiet */ +} + /* * Common option parsing function for CREATE and ALTER SUBSCRIPTION commands. * @@ -132,7 +188,7 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, if (IsSet(supported_opts, SUBOPT_BINARY)) opts->binary = false; if (IsSet(supported_opts, SUBOPT_STREAMING)) - opts->streaming = false; + opts->streaming = SUBSTREAM_OFF; if (IsSet(supported_opts, SUBOPT_TWOPHASE_COMMIT)) opts->twophase = false; if (IsSet(supported_opts, SUBOPT_DISABLE_ON_ERR)) @@ -233,7 +289,7 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, errorConflictingDefElem(defel, pstate); opts->specified_opts |= SUBOPT_STREAMING; - opts->streaming = defGetBoolean(defel); + opts->streaming = defGetStreamingMode(defel); } else if (strcmp(defel->defname, "two_phase") == 0) { @@ -601,7 +657,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, values[Anum_pg_subscription_subowner - 1] = ObjectIdGetDatum(owner); 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_substream - 1] = CharGetDatum(opts.streaming); values[Anum_pg_subscription_subtwophasestate - 1] = CharGetDatum(opts.twophase ? LOGICALREP_TWOPHASE_STATE_PENDING : @@ -1060,7 +1116,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, if (IsSet(opts.specified_opts, SUBOPT_STREAMING)) { values[Anum_pg_subscription_substream - 1] = - BoolGetDatum(opts.streaming); + CharGetDatum(opts.streaming); replaces[Anum_pg_subscription_substream - 1] = true; } diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c index 40601aefd9..40ccb8993c 100644 --- a/src/backend/postmaster/bgworker.c +++ b/src/backend/postmaster/bgworker.c @@ -128,6 +128,9 @@ static const struct }, { "ApplyWorkerMain", ApplyWorkerMain + }, + { + "ApplyBgworkerMain", ApplyBgworkerMain } }; diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile index c4e2fdeb71..a24a41969e 100644 --- a/src/backend/replication/logical/Makefile +++ b/src/backend/replication/logical/Makefile @@ -26,6 +26,7 @@ OBJS = \ reorderbuffer.o \ snapbuild.o \ tablesync.o \ - worker.o + worker.o \ + applybgwroker.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/replication/logical/applybgwroker.c b/src/backend/replication/logical/applybgwroker.c new file mode 100644 index 0000000000..8af64974f2 --- /dev/null +++ b/src/backend/replication/logical/applybgwroker.c @@ -0,0 +1,756 @@ +/*------------------------------------------------------------------------- + * applybgwroker.c + * Support routines for applying xact by apply background worker + * + * Copyright (c) 2016-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/replication/logical/applybgwroker.c + * + * This file contains routines that are intended to support setting up, using, + * and tearing down a ApplyBgworkerState. + * Refer to the comments in file header of logical/worker.c to see more + * informations about apply background worker. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "libpq/pqformat.h" +#include "mb/pg_wchar.h" +#include "pgstat.h" +#include "postmaster/interrupt.h" +#include "replication/logicalworker.h" +#include "replication/origin.h" +#include "replication/walreceiver.h" +#include "replication/worker_internal.h" +#include "storage/ipc.h" +#include "storage/procarray.h" +#include "tcop/tcopprot.h" +#include "utils/inval.h" +#include "utils/memutils.h" +#include "utils/resowner.h" +#include "utils/syscache.h" + +#define PG_LOGICAL_APPLY_SHM_MAGIC 0x79fb2447 // TODO Consider change + +/* + * DSM keys for apply background worker. Unlike other parallel execution code, + * since we don't need to worry about DSM keys conflicting with plan_node_id we + * can use small integers. + */ +#define APPLY_BGWORKER_KEY_SHARED 1 +#define APPLY_BGWORKER_KEY_MQ 2 + +/* + * entry for a hash table we use to map from xid to our apply background worker + * state. + */ +typedef struct ApplyBgworkerEntry +{ + TransactionId xid; + ApplyBgworkerState *wstate; +} ApplyBgworkerEntry; + +/* Apply background workers hash table (initialized on first use) */ +static HTAB *ApplyWorkersHash = NULL; +static List *ApplyWorkersFreeList = NIL; +static List *ApplyWorkersList = NIL; + +/* + * Fields to record the share informations between main apply worker and apply + * background worker. + */ +volatile ApplyBgworkerShared *MyParallelState = NULL; + +List *subxactlist = NIL; + +/* apply background worker setup */ +static ApplyBgworkerState *apply_bgworker_setup(void); +static void apply_bgworker_setup_dsm(ApplyBgworkerState *wstate); + +/* + * Look up worker inside ApplyWorkersHash for requested xid. + * + * If start flag is true, try to start a new worker if not found in hash table. + */ +ApplyBgworkerState * +apply_bgworker_find_or_start(TransactionId xid, bool start) +{ + bool found; + ApplyBgworkerState *wstate; + ApplyBgworkerEntry *entry = NULL; + + if (!TransactionIdIsValid(xid)) + return NULL; + + /* + * We don't start new background worker if we are not in streaming apply + * mode. + */ + if (MySubscription->stream != SUBSTREAM_APPLY) + return NULL; + + /* + * We don't start new background worker if user has set skiplsn as it's + * possible that user want to skip the streaming transaction. For + * streaming transaction, we need to spill the transaction to disk so that + * we can get the last LSN of the transaction to judge whether to skip + * before starting to apply the change. + */ + if (start && !XLogRecPtrIsInvalid(MySubscription->skiplsn)) + return NULL; + + /* + * For streaming transactions that are being applied in apply background + * worker, we cannot decide whether to apply the change for a relation + * that is not in the READY state (see should_apply_changes_for_rel) as we + * won't know remote_final_lsn by that time. So, we don't start new apply + * background worker in this case. + */ + if (start && !AllTablesyncsReady()) + return NULL; + + /* First time through, initialize apply workers hashtable */ + if (ApplyWorkersHash == NULL) + { + HASHCTL ctl; + + MemSet(&ctl, 0, sizeof(ctl)); + ctl.keysize = sizeof(TransactionId); + ctl.entrysize = sizeof(ApplyBgworkerEntry); + ctl.hcxt = ApplyContext; + + ApplyWorkersHash = hash_create("logical apply workers hash", 8, &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + } + + /* + * Find entry for requested transaction. + */ + entry = hash_search(ApplyWorkersHash, &xid, start ? HASH_ENTER : HASH_FIND, + &found); + if (found) + { + entry->wstate->pstate->status = APPLY_BGWORKER_BUSY; + return entry->wstate; + } + else if (!start) + return NULL; + + /* + * Now, we try to get a apply background worker. If there is at least one + * worker in the idle list, then take one. Otherwise, we try to start a + * new apply background worker. + */ + if (list_length(ApplyWorkersFreeList) > 0) + { + wstate = (ApplyBgworkerState *) llast(ApplyWorkersFreeList); + ApplyWorkersFreeList = list_delete_last(ApplyWorkersFreeList); + } + else + { + wstate = apply_bgworker_setup(); + + if (wstate == NULL) + { + /* + * If the apply background worker cannot be launched, remove entry + * in hash table. + */ + hash_search(ApplyWorkersHash, &xid, HASH_REMOVE, &found); + return NULL; + } + } + + /* Fill up the hash entry */ + wstate->pstate->status = APPLY_BGWORKER_BUSY; + wstate->pstate->server_version = walrcv_server_version(LogRepWorkerWalRcvConn); + wstate->pstate->stream_xid = xid; + entry->wstate = wstate; + entry->xid = xid; + + return wstate; +} + +/* + * Add the worker to the free list and remove the entry from hash table. + */ +void +apply_bgworker_free(ApplyBgworkerState *wstate) +{ + MemoryContext oldctx; + TransactionId xid = wstate->pstate->stream_xid; + + Assert(wstate->pstate->status == APPLY_BGWORKER_FINISHED); + + oldctx = MemoryContextSwitchTo(ApplyContext); + + hash_search(ApplyWorkersHash, &xid, HASH_REMOVE, NULL); + + elog(DEBUG1, "adding finished apply worker #%u for xid %u to the idle list", + wstate->pstate->n, wstate->pstate->stream_xid); + + ApplyWorkersFreeList = lappend(ApplyWorkersFreeList, wstate); + + MemoryContextSwitchTo(oldctx); +} + +/* Apply Background Worker main loop */ +static void +LogicalApplyBgwLoop(shm_mq_handle *mqh, volatile ApplyBgworkerShared *pst) +{ + shm_mq_result shmq_res; + PGPROC *registrant; + ErrorContextCallback errcallback; + XLogRecPtr last_received = InvalidXLogRecPtr; + + registrant = BackendPidGetProc(MyBgworkerEntry->bgw_notify_pid); + SetLatch(®istrant->procLatch); + + /* + * Push apply error context callback. Fields will be filled during + * applying a change. + */ + errcallback.callback = apply_error_callback; + errcallback.previous = error_context_stack; + error_context_stack = &errcallback; + + for (;;) + { + void *data; + Size len; + int c; + StringInfoData s; + MemoryContext oldctx; + XLogRecPtr start_lsn; + XLogRecPtr end_lsn; + TimestampTz send_time; + + CHECK_FOR_INTERRUPTS(); + + /* Ensure we are reading the data into our memory context. */ + oldctx = MemoryContextSwitchTo(ApplyMessageContext); + + shmq_res = shm_mq_receive(mqh, &len, &data, false); + + if (shmq_res != SHM_MQ_SUCCESS) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("lost connection to the main apply worker"))); + + if (len == 0) + break; + + s.cursor = 0; + s.maxlen = -1; + s.data = (char *) data; + s.len = len; + + /* + * We use first byte of message for additional communication between + * main Logical replication worker and apply bgworkers, so if it + * differs from 'w', then process it first. + */ + c = pq_getmsgbyte(&s); + switch (c) + { + /* End message of streaming chunk */ + case LOGICAL_REP_MSG_STREAM_STOP: + elog(DEBUG1, "[Apply BGW #%u] ended processing streaming chunk," + "waiting on shm_mq_receive", pst->n); + + apply_bgworker_set_status(APPLY_BGWORKER_READY); + + SetLatch(®istrant->procLatch); + + in_streamed_transaction = false; + pgstat_report_activity(STATE_IDLE, NULL); + continue; + case 'w': + break; + default: + elog(ERROR, "[Apply BGW #%u] unexpected message \"%c\"", + pst->n, c); + break; + } + + start_lsn = pq_getmsgint64(&s); + end_lsn = pq_getmsgint64(&s); + send_time = pq_getmsgint64(&s); + + if (last_received < start_lsn) + last_received = start_lsn; + + if (last_received < end_lsn) + last_received = end_lsn; + + /* + * TO IMPROVE: Do we need to display the apply background worker's + * information in pg_stat_replication ? + */ + UpdateWorkerStats(last_received, send_time, false); + + apply_dispatch(&s); + + if (ConfigReloadPending) + { + ConfigReloadPending = false; + ProcessConfigFile(PGC_SIGHUP); + } + + MemoryContextSwitchTo(oldctx); + MemoryContextReset(ApplyMessageContext); + } + + MemoryContextSwitchTo(TopMemoryContext); + MemoryContextReset(ApplyContext); + + /* Pop the error context stack */ + error_context_stack = errcallback.previous; + + elog(DEBUG1, "[Apply BGW #%u] exiting", pst->n); + + /* Signal main process that we are done. */ + SetLatch(®istrant->procLatch); +} + +/* + * Set the exit status so that the main apply worker can realize we have + * shutdown. + */ +static void +ApplyBgwShutdown(int code, Datum arg) +{ + SpinLockAcquire(&MyParallelState->mutex); + MyParallelState->status = APPLY_BGWORKER_EXIT; + SpinLockRelease(&MyParallelState->mutex); + + dsm_detach((dsm_segment *) DatumGetPointer(arg)); +} + +/* + * Apply Background Worker entry point + */ +void +ApplyBgworkerMain(Datum main_arg) +{ + volatile ApplyBgworkerShared *pst; + + dsm_handle handle; + dsm_segment *seg; + shm_toc *toc; + shm_mq *mq; + shm_mq_handle *mqh; + MemoryContext oldcontext; + RepOriginId originid; + int worker_slot = DatumGetInt32(main_arg); + char originname[NAMEDATALEN]; + + /* Attach to slot */ + logicalrep_worker_attach(worker_slot); + + MemoryContextSwitchTo(TopMemoryContext); + + /* Load the subscription into persistent memory context. */ + ApplyContext = AllocSetContextCreate(TopMemoryContext, + "ApplyContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Init the ApplyMessageContext which we clean up after each replication + * protocol message. + */ + ApplyMessageContext = AllocSetContextCreate(ApplyContext, + "ApplyMessageContext", + ALLOCSET_DEFAULT_SIZES); + + /* Setup signal handling */ + pqsignal(SIGHUP, SignalHandlerForConfigReload); + pqsignal(SIGTERM, die); + BackgroundWorkerUnblockSignals(); + + /* + * Connect to the dynamic shared memory segment. + * + * The backend that registered this worker passed us the ID of a shared + * memory segment to which we must attach for further instructions. In + * order to attach to dynamic shared memory, we need a resource owner. + * Once we've mapped the segment in our address space, attach to the table + * of contents so we can locate the various data structures we'll need to + * find within the segment. + */ + CurrentResourceOwner = ResourceOwnerCreate(NULL, "Logical apply worker"); + memcpy(&handle, MyBgworkerEntry->bgw_extra, sizeof(dsm_handle)); + seg = dsm_attach(handle); + if (seg == NULL) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("unable to map dynamic shared memory segment"))); + toc = shm_toc_attach(PG_LOGICAL_APPLY_SHM_MAGIC, dsm_segment_address(seg)); + if (toc == NULL) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("bad magic number in dynamic shared memory segment"))); + + before_shmem_exit(ApplyBgwShutdown, PointerGetDatum(seg)); + + /* Look up the parallel state. */ + pst = shm_toc_lookup(toc, APPLY_BGWORKER_KEY_SHARED, false); + MyParallelState = pst; + + /* + * Attach to the message queue. + */ + mq = shm_toc_lookup(toc, APPLY_BGWORKER_KEY_MQ, false); + shm_mq_set_receiver(mq, MyProc); + mqh = shm_mq_attach(mq, seg, NULL); + + /* Run as replica session replication role. */ + SetConfigOption("session_replication_role", "replica", + PGC_SUSET, PGC_S_OVERRIDE); + + /* Connect to our database. */ + BackgroundWorkerInitializeConnectionByOid(MyLogicalRepWorker->dbid, + MyLogicalRepWorker->userid, + 0); + + /* + * Set always-secure search path, so malicious users can't redirect user + * code (e.g. pg_index.indexprs). + */ + SetConfigOption("search_path", "", PGC_SUSET, PGC_S_OVERRIDE); + + /* + * Set the client encoding to the database encoding, since that is what + * the leader will expect. + */ + SetClientEncoding(GetDatabaseEncoding()); + + stream_xid = pst->stream_xid; + + StartTransactionCommand(); + oldcontext = MemoryContextSwitchTo(ApplyContext); + + MySubscription = GetSubscription(MyLogicalRepWorker->subid, true); + if (!MySubscription) + { + ereport(LOG, + (errmsg("logical replication apply worker for subscription %u will not " + "start because the subscription was removed during startup", + MyLogicalRepWorker->subid))); + proc_exit(0); + } + + MySubscriptionValid = true; + MemoryContextSwitchTo(oldcontext); + + /* Setup synchronous commit according to the user's wishes */ + SetConfigOption("synchronous_commit", MySubscription->synccommit, + PGC_BACKEND, PGC_S_OVERRIDE); + + /* Keep us informed about subscription changes. */ + CacheRegisterSyscacheCallback(SUBSCRIPTIONOID, + subscription_change_cb, + (Datum) 0); + + CommitTransactionCommand(); + + /* Setup replication origin tracking. */ + StartTransactionCommand(); + snprintf(originname, sizeof(originname), "pg_%u", MySubscription->oid); + originid = replorigin_by_name(originname, true); + if (!OidIsValid(originid)) + originid = replorigin_create(originname); + + /* + * The apply background worker doesn't need to monopolize this replication + * origin which was already acquired by its leader process. + */ + replorigin_session_setup(originid, false); + replorigin_session_origin = originid; + CommitTransactionCommand(); + + /* + * Allocate the origin name in long-lived context for error context + * message. + */ + apply_error_callback_arg.origin_name = MemoryContextStrdup(ApplyContext, + originname); + + /* + * Indicate that we're fully initialized and ready to begin the main part + * of the apply operation. + */ + apply_bgworker_set_status(APPLY_BGWORKER_ATTACHED); + + elog(DEBUG1, "[Apply BGW #%u] started", pst->n); + + LogicalApplyBgwLoop(mqh, pst); + + /* + * We're done. Explicitly detach the shared memory segment so that we + * don't get a resource leak warning at commit time. This will fire any + * on_dsm_detach callbacks we've registered, as well. Once that's done, + * we can go ahead and exit. + */ + dsm_detach(seg); + proc_exit(0); +} + +/* + * Set up a dynamic shared memory segment. + * + * We set up a control region that contains a ApplyBgworkerShared, + * plus one region per message queue. There are as many message queues as + * the number of workers. + */ +static void +apply_bgworker_setup_dsm(ApplyBgworkerState *wstate) +{ + shm_toc_estimator e; + Size segsize; + dsm_segment *seg; + shm_toc *toc; + ApplyBgworkerShared *pst; + shm_mq *mq; + int64 queue_size = 160000000; /* 16 MB for now */ + + /* + * Estimate how much shared memory we need. + * + * Because the TOC machinery may choose to insert padding of oddly-sized + * requests, we must estimate each chunk separately. + * + * We need one key to register the location of the header, and we need + * another key to track the location of the message queue. + */ + shm_toc_initialize_estimator(&e); + shm_toc_estimate_chunk(&e, sizeof(ApplyBgworkerShared)); + shm_toc_estimate_chunk(&e, (Size) queue_size); + + shm_toc_estimate_keys(&e, 1 + 1); + segsize = shm_toc_estimate(&e); + + /* Create the shared memory segment and establish a table of contents. */ + seg = dsm_create(shm_toc_estimate(&e), 0); + toc = shm_toc_create(PG_LOGICAL_APPLY_SHM_MAGIC, dsm_segment_address(seg), + segsize); + + /* Set up the header region. */ + pst = shm_toc_allocate(toc, sizeof(ApplyBgworkerShared)); + SpinLockInit(&pst->mutex); + pst->status = APPLY_BGWORKER_BUSY; + pst->server_version = walrcv_server_version(LogRepWorkerWalRcvConn); + pst->stream_xid = stream_xid; + pst->n = list_length(ApplyWorkersList) + 1; + + shm_toc_insert(toc, APPLY_BGWORKER_KEY_SHARED, pst); + + /* Set up one message queue per worker, plus one. */ + mq = shm_mq_create(shm_toc_allocate(toc, (Size) queue_size), + (Size) queue_size); + shm_toc_insert(toc, APPLY_BGWORKER_KEY_MQ, mq); + shm_mq_set_sender(mq, MyProc); + + /* Attach the queues. */ + wstate->mq_handle = shm_mq_attach(mq, seg, NULL); + + /* Return results to caller. */ + wstate->dsm_seg = seg; + wstate->pstate = pst; +} + +/* + * Start apply background worker process and allocate shared memory for it. + */ +static ApplyBgworkerState * +apply_bgworker_setup(void) +{ + MemoryContext oldcontext; + bool launched; + ApplyBgworkerState *wstate; + + elog(DEBUG1, "setting up apply worker #%u", list_length(ApplyWorkersList) + 1); + + oldcontext = MemoryContextSwitchTo(ApplyContext); + + wstate = (ApplyBgworkerState *) palloc0(sizeof(ApplyBgworkerState)); + + /* Setup shared memory */ + apply_bgworker_setup_dsm(wstate); + + launched = logicalrep_worker_launch(MyLogicalRepWorker->dbid, + MySubscription->oid, + MySubscription->name, + MyLogicalRepWorker->userid, + InvalidOid, + dsm_segment_handle(wstate->dsm_seg)); + + if (launched) + { + /* Wait for worker to attach. */ + apply_bgworker_wait_for(wstate, APPLY_BGWORKER_ATTACHED); + + ApplyWorkersList = lappend(ApplyWorkersList, wstate); + } + else + { + pfree(wstate); + wstate = NULL; + } + + MemoryContextSwitchTo(oldcontext); + + return wstate; +} + +/* + * Send the data to the specified apply background worker via shared-memory queue. + */ +void +apply_bgworker_send_data(ApplyBgworkerState *wstate, Size nbytes, const void *data) +{ + shm_mq_result result; + + result = shm_mq_send(wstate->mq_handle, nbytes, data, false, true); + + if (result != SHM_MQ_SUCCESS) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not send tuples to shared-memory queue"))); +} + +/* + * Wait until the status of apply background worker reaches the + * 'wait_for_status' + */ +void +apply_bgworker_wait_for(ApplyBgworkerState *wstate, + ApplyBgworkerStatus wait_for_status) +{ + for (;;) + { + char status; + + SpinLockAcquire(&wstate->pstate->mutex); + status = wstate->pstate->status; + SpinLockRelease(&wstate->pstate->mutex); + + /* Done if already in correct status. */ + if (status == wait_for_status) + break; + + /* If any workers (or the postmaster) have died, we have failed. */ + if (status == APPLY_BGWORKER_EXIT) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("background worker %u failed to apply transaction %u", + wstate->pstate->n, wstate->pstate->stream_xid))); + + /* Wait to be signalled. */ + WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, + WAIT_EVENT_LOGICAL_APPLY_WORKER_STATE_CHANGE); + + /* Reset the latch so we don't spin. */ + ResetLatch(MyLatch); + + /* An interrupt may have occurred while we were waiting. */ + CHECK_FOR_INTERRUPTS(); + } +} + +/* + * Check the status of workers and report an error if any apply background + * worker has exited unexpectedly. + * + * Exit if any relation is not in the READY state and if any worker is handling + * the streaming transaction at the same time. Because for streaming + * transactions that is being applied in apply background worker, we cannot + * decide whether to apply the change for a relation that is not in the READY + * state (see should_apply_changes_for_rel) as we won't know remote_final_lsn + * by that time. + */ +void +apply_bgworker_check_status(void) +{ + ListCell *lc; + + if (am_apply_bgworker() || MySubscription->stream != SUBSTREAM_APPLY) + return; + + foreach(lc, ApplyWorkersList) + { + ApplyBgworkerState *wstate = (ApplyBgworkerState *) lfirst(lc); + + /* + * We don't lock here as in the worst case we will just detect the + * failure of worker a bit later. + */ + if (wstate->pstate->status == APPLY_BGWORKER_EXIT) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("background worker %u exited unexpectedly", + wstate->pstate->n))); + } + + if (list_length(ApplyWorkersFreeList) != list_length(ApplyWorkersList) && + !AllTablesyncsReady()) + { + ereport(LOG, + (errmsg("logical replication apply workers for subscription \"%s\" will restart", + MySubscription->name), + errdetail("Cannot handle streamed replication transaction by apply " + "bgworkers until all tables are synchronized"))); + + proc_exit(0); + } +} + +/* Set the apply background worker status */ +void +apply_bgworker_set_status(ApplyBgworkerStatus status) +{ + if (!am_apply_bgworker()) + return; + + elog(DEBUG1, "[Apply BGW #%u] set status to %d", MyParallelState->n, status); + + SpinLockAcquire(&MyParallelState->mutex); + MyParallelState->status = status; + SpinLockRelease(&MyParallelState->mutex); +} + +/* + * Define a savepoint for a subxact in apply background worker if needed. + * + * Inside apply background worker we can figure out that new subtransaction was + * started if new change arrived with different xid. In that case we can define + * named savepoint, so that we were able to commit/rollback it separately + * later. + * Special case is if the first change comes from subtransaction, then + * we check that current_xid differs from stream_xid. + */ +void +apply_bgworker_subxact_info_add(TransactionId current_xid) +{ + if (current_xid != stream_xid && + !list_member_int(subxactlist, (int) current_xid)) + { + MemoryContext oldctx; + char spname[MAXPGPATH]; + + snprintf(spname, MAXPGPATH, "savepoint_for_xid_%u", current_xid); + + elog(DEBUG1, "[Apply BGW #%u] defining savepoint %s", + MyParallelState->n, spname); + + DefineSavepoint(spname); + CommitTransactionCommand(); + + oldctx = MemoryContextSwitchTo(ApplyContext); + subxactlist = lappend_int(subxactlist, (int) current_xid); + MemoryContextSwitchTo(oldctx); + } +} diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c index aa2427ba73..00fdde0830 100644 --- a/src/backend/replication/logical/decode.c +++ b/src/backend/replication/logical/decode.c @@ -651,9 +651,10 @@ DecodeCommit(LogicalDecodingContext *ctx, XLogRecordBuffer *buf, { for (i = 0; i < parsed->nsubxacts; i++) { - ReorderBufferForget(ctx->reorder, parsed->subxacts[i], buf->origptr); + ReorderBufferForget(ctx->reorder, parsed->subxacts[i], buf->origptr, + commit_time); } - ReorderBufferForget(ctx->reorder, xid, buf->origptr); + ReorderBufferForget(ctx->reorder, xid, buf->origptr, commit_time); return; } @@ -821,10 +822,11 @@ DecodeAbort(LogicalDecodingContext *ctx, XLogRecordBuffer *buf, for (i = 0; i < parsed->nsubxacts; i++) { ReorderBufferAbort(ctx->reorder, parsed->subxacts[i], - buf->record->EndRecPtr); + buf->record->EndRecPtr, abort_time); } - ReorderBufferAbort(ctx->reorder, xid, buf->record->EndRecPtr); + ReorderBufferAbort(ctx->reorder, xid, buf->record->EndRecPtr, + abort_time); } /* update the decoding stats */ diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c index 2bdab53e19..201f51232e 100644 --- a/src/backend/replication/logical/launcher.c +++ b/src/backend/replication/logical/launcher.c @@ -73,6 +73,7 @@ static void logicalrep_launcher_onexit(int code, Datum arg); static void logicalrep_worker_onexit(int code, Datum arg); static void logicalrep_worker_detach(void); static void logicalrep_worker_cleanup(LogicalRepWorker *worker); +static void logicalrep_worker_stop_internal(LogicalRepWorker *worker); static bool on_commit_launcher_wakeup = false; @@ -223,6 +224,10 @@ logicalrep_worker_find(Oid subid, Oid relid, bool only_running) { LogicalRepWorker *w = &LogicalRepCtx->workers[i]; + /* We only need main apply worker or table sync worker here */ + if (w->subworker) + continue; + if (w->in_use && w->subid == subid && w->relid == relid && (!only_running || w->proc)) { @@ -259,11 +264,11 @@ logicalrep_workers_find(Oid subid, bool only_running) } /* - * Start new apply background worker, if possible. + * Start new background worker, if possible. */ -void +bool logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid, - Oid relid) + Oid relid, dsm_handle subworker_dsm) { BackgroundWorker bgw; BackgroundWorkerHandle *bgw_handle; @@ -273,6 +278,10 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid, LogicalRepWorker *worker = NULL; int nsyncworkers; TimestampTz now; + bool is_subworker = (subworker_dsm != DSM_HANDLE_INVALID); + + /* We don't support table sync in subworker */ + Assert(!(is_subworker && OidIsValid(relid))); ereport(DEBUG1, (errmsg_internal("starting logical replication worker for subscription \"%s\"", @@ -350,7 +359,7 @@ retry: if (OidIsValid(relid) && nsyncworkers >= max_sync_workers_per_subscription) { LWLockRelease(LogicalRepWorkerLock); - return; + return false; } /* @@ -364,7 +373,7 @@ retry: (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("out of logical replication worker slots"), errhint("You might need to increase max_logical_replication_workers."))); - return; + return false; } /* Prepare the worker slot. */ @@ -379,6 +388,7 @@ retry: worker->relstate = SUBREL_STATE_UNKNOWN; worker->relstate_lsn = InvalidXLogRecPtr; worker->stream_fileset = NULL; + worker->subworker = is_subworker; worker->last_lsn = InvalidXLogRecPtr; TIMESTAMP_NOBEGIN(worker->last_send_time); TIMESTAMP_NOBEGIN(worker->last_recv_time); @@ -396,19 +406,30 @@ retry: BGWORKER_BACKEND_DATABASE_CONNECTION; bgw.bgw_start_time = BgWorkerStart_RecoveryFinished; snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres"); - snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain"); + + if (!is_subworker) + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain"); + else + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyBgworkerMain"); + if (OidIsValid(relid)) snprintf(bgw.bgw_name, BGW_MAXLEN, "logical replication worker for subscription %u sync %u", subid, relid); - else + else if (!is_subworker) snprintf(bgw.bgw_name, BGW_MAXLEN, "logical replication worker for subscription %u", subid); + else + snprintf(bgw.bgw_name, BGW_MAXLEN, + "logical replication background apply worker for subscription %u ", subid); snprintf(bgw.bgw_type, BGW_MAXLEN, "logical replication worker"); bgw.bgw_restart_time = BGW_NEVER_RESTART; bgw.bgw_notify_pid = MyProcPid; bgw.bgw_main_arg = Int32GetDatum(slot); + if (is_subworker) + memcpy(bgw.bgw_extra, &subworker_dsm, sizeof(dsm_handle)); + if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle)) { /* Failed to start worker, so clean up the worker slot. */ @@ -421,11 +442,13 @@ retry: (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("out of background worker slots"), errhint("You might need to increase max_worker_processes."))); - return; + return false; } /* Now wait until it attaches. */ WaitForReplicationWorkerAttach(worker, generation, bgw_handle); + + return true; } /* @@ -436,7 +459,6 @@ void logicalrep_worker_stop(Oid subid, Oid relid) { LogicalRepWorker *worker; - uint16 generation; LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); @@ -449,6 +471,22 @@ logicalrep_worker_stop(Oid subid, Oid relid) return; } + logicalrep_worker_stop_internal(worker); + + LWLockRelease(LogicalRepWorkerLock); +} + +/* + * Workhorse for logicalrep_worker_stop() and logicalrep_worker_detach(). Stop + * the worker and wait for it to die. + */ +static void +logicalrep_worker_stop_internal(LogicalRepWorker *worker) +{ + uint16 generation; + + Assert(LWLockHeldByMe(LogicalRepWorkerLock)); + /* * Remember which generation was our worker so we can check if what we see * is still the same one. @@ -485,10 +523,7 @@ logicalrep_worker_stop(Oid subid, Oid relid) * different, meaning that a different worker has taken the slot. */ if (!worker->in_use || worker->generation != generation) - { - LWLockRelease(LogicalRepWorkerLock); return; - } /* Worker has assigned proc, so it has started. */ if (worker->proc) @@ -522,8 +557,6 @@ logicalrep_worker_stop(Oid subid, Oid relid) LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); } - - LWLockRelease(LogicalRepWorkerLock); } /* @@ -599,6 +632,29 @@ logicalrep_worker_attach(int slot) static void logicalrep_worker_detach(void) { + /* + * If we are the main apply worker, stop all the apply background workers + * we started before. + */ + if (!MyLogicalRepWorker->subworker) + { + List *workers; + ListCell *lc; + + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); + + workers = logicalrep_workers_find(MyLogicalRepWorker->subid, true); + foreach(lc, workers) + { + LogicalRepWorker *w = (LogicalRepWorker *) lfirst(lc); + + if (w->subworker) + logicalrep_worker_stop_internal(w); + } + + LWLockRelease(LogicalRepWorkerLock); + } + /* Block concurrent access. */ LWLockAcquire(LogicalRepWorkerLock, LW_EXCLUSIVE); @@ -621,6 +677,7 @@ logicalrep_worker_cleanup(LogicalRepWorker *worker) worker->userid = InvalidOid; worker->subid = InvalidOid; worker->relid = InvalidOid; + worker->subworker = false; } /* @@ -868,7 +925,7 @@ ApplyLauncherMain(Datum main_arg) wait_time = wal_retrieve_retry_interval; logicalrep_worker_launch(sub->dbid, sub->oid, sub->name, - sub->owner, InvalidOid); + sub->owner, InvalidOid, DSM_HANDLE_INVALID); } } diff --git a/src/backend/replication/logical/origin.c b/src/backend/replication/logical/origin.c index 21937ab2d3..9273011b0a 100644 --- a/src/backend/replication/logical/origin.c +++ b/src/backend/replication/logical/origin.c @@ -1063,12 +1063,21 @@ ReplicationOriginExitCleanup(int code, Datum arg) * array doesn't have to be searched when calling * replorigin_session_advance(). * - * Obviously only one such cached origin can exist per process and the current + * Normally only one such cached origin can exist per process and the current * cached value can only be set again after the previous value is torn down * with replorigin_session_reset(). + * + * However, If must_acquire is false, we allow process to get the slot which is + * already acquired by other process. It's safe because 1) The only caller + * (apply background workers) will maintain the commit order by allowing only + * one process to commit at a time, so no two workers will be operating on the + * same origin at the same time (see comments in logical/worker.c). 2) Even + * though we try to advance the session's origin concurrently, it's safe to do + * so as we change/advance the session_origin LSNs under replicate_state + * LWLock. */ void -replorigin_session_setup(RepOriginId node) +replorigin_session_setup(RepOriginId node, bool must_acquire) { static bool registered_cleanup; int i; @@ -1110,7 +1119,7 @@ replorigin_session_setup(RepOriginId node) if (curstate->roident != node) continue; - else if (curstate->acquired_by != 0) + else if (curstate->acquired_by != 0 && must_acquire) { ereport(ERROR, (errcode(ERRCODE_OBJECT_IN_USE), @@ -1141,7 +1150,14 @@ replorigin_session_setup(RepOriginId node) Assert(session_replication_state->roident != InvalidRepOriginId); - session_replication_state->acquired_by = MyProcPid; + if (must_acquire) + session_replication_state->acquired_by = MyProcPid; + else if (session_replication_state->acquired_by == 0) + ereport(ERROR, + (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), + errmsg("could not find correct replication state slot for replication origin with OID %u for apply background worker", + node), + errhint("There is no replication state slot set by its main apply worker."))); LWLockRelease(ReplicationOriginLock); @@ -1321,7 +1337,7 @@ pg_replication_origin_session_setup(PG_FUNCTION_ARGS) name = text_to_cstring((text *) DatumGetPointer(PG_GETARG_DATUM(0))); origin = replorigin_by_name(name, false); - replorigin_session_setup(origin); + replorigin_session_setup(origin, true); replorigin_session_origin = origin; diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c index ff8513e2d2..a888038eb4 100644 --- a/src/backend/replication/logical/proto.c +++ b/src/backend/replication/logical/proto.c @@ -1166,28 +1166,47 @@ logicalrep_read_stream_commit(StringInfo in, LogicalRepCommitData *commit_data) */ void logicalrep_write_stream_abort(StringInfo out, TransactionId xid, - TransactionId subxid) + ReorderBufferTXN *txn, XLogRecPtr abort_lsn) { pq_sendbyte(out, LOGICAL_REP_MSG_STREAM_ABORT); - Assert(TransactionIdIsValid(xid) && TransactionIdIsValid(subxid)); + Assert(TransactionIdIsValid(xid) && TransactionIdIsValid(txn->xid)); /* transaction ID */ pq_sendint32(out, xid); - pq_sendint32(out, subxid); + pq_sendint32(out, txn->xid); + pq_sendint64(out, abort_lsn); + pq_sendint64(out, txn->xact_time.abort_time); } /* * Read STREAM ABORT from the output stream. */ void -logicalrep_read_stream_abort(StringInfo in, TransactionId *xid, - TransactionId *subxid) +logicalrep_read_stream_abort(StringInfo in, + LogicalRepStreamAbortData *abort_data, + bool include_abort_lsn) { - Assert(xid && subxid); + Assert(abort_data); + + abort_data->xid = pq_getmsgint(in, 4); + abort_data->subxid = pq_getmsgint(in, 4); - *xid = pq_getmsgint(in, 4); - *subxid = pq_getmsgint(in, 4); + /* + * If the version of the publisher is lower than the version of the + * subscriber, it may not support sending these two fields, so only take + * these fields when include_abort_lsn is true. + */ + if (include_abort_lsn) + { + abort_data->abort_lsn = pq_getmsgint64(in); + abort_data->abort_time = pq_getmsgint64(in); + } + else + { + abort_data->abort_lsn = InvalidXLogRecPtr; + abort_data->abort_time = 0; + } } /* diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c index 8da5f9089c..3d2bbdb38d 100644 --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -2826,7 +2826,8 @@ ReorderBufferFinishPrepared(ReorderBuffer *rb, TransactionId xid, * disk. */ void -ReorderBufferAbort(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn) +ReorderBufferAbort(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn, + TimestampTz abort_time) { ReorderBufferTXN *txn; @@ -2837,6 +2838,8 @@ ReorderBufferAbort(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn) if (txn == NULL) return; + txn->xact_time.abort_time = abort_time; + /* For streamed transactions notify the remote node about the abort. */ if (rbtxn_is_streamed(txn)) { @@ -2911,7 +2914,8 @@ ReorderBufferAbortOld(ReorderBuffer *rb, TransactionId oldestRunningXid) * to this xid might re-create the transaction incompletely. */ void -ReorderBufferForget(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn) +ReorderBufferForget(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn, + TimestampTz abort_time) { ReorderBufferTXN *txn; @@ -2922,6 +2926,8 @@ ReorderBufferForget(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn) if (txn == NULL) return; + txn->xact_time.abort_time = abort_time; + /* For streamed transactions notify the remote node about the abort. */ if (rbtxn_is_streamed(txn)) rb->stream_abort(rb, txn, lsn); diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c index 670c6fcada..8ffba7e2e5 100644 --- a/src/backend/replication/logical/tablesync.c +++ b/src/backend/replication/logical/tablesync.c @@ -568,7 +568,8 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) MySubscription->oid, MySubscription->name, MyLogicalRepWorker->userid, - rstate->relid); + rstate->relid, + DSM_HANDLE_INVALID); hentry->last_start_time = now; } } @@ -589,6 +590,9 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) void process_syncing_tables(XLogRecPtr current_lsn) { + if (MyLogicalRepWorker->subworker) + return; + if (am_tablesync_worker()) process_syncing_tables_for_sync(current_lsn); else @@ -1273,7 +1277,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) * time this tablesync was launched. */ originid = replorigin_by_name(originname, false); - replorigin_session_setup(originid); + replorigin_session_setup(originid, true); replorigin_session_origin = originid; *origin_startpos = replorigin_session_get_progress(false); @@ -1384,7 +1388,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) true /* go backward */ , true /* WAL log */ ); UnlockRelationOid(ReplicationOriginRelationId, RowExclusiveLock); - replorigin_session_setup(originid); + replorigin_session_setup(originid, true); replorigin_session_origin = originid; } else diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index fc210a9e7b..32bf516ebf 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -22,8 +22,25 @@ * STREAMED TRANSACTIONS * --------------------- * Streamed transactions (large transactions exceeding a memory limit on the - * upstream) are not applied immediately, but instead, the data is written - * to temporary files and then applied at once when the final commit arrives. + * upstream) are applied via one of two approaches. + * + * 1) Separate background workers + * + * Assign a new apply background worker (if available) as soon as the xact's + * first stream is received and the main apply worker will send changes to this + * new worker via shared memory. We keep this worker assigned till the + * transaction commit is received and also wait for the worker to finish at + * commit. This preserves commit ordering and avoids writing to and reading + * from file in most cases. We still need to spill if there is no worker + * available. We also need to allow stream_stop to complete by the background + * worker to avoid deadlocks because T-1's current stream of changes can update + * rows in conflicting order with T-2's next stream of changes. + * + * 2) Write to temporary files and apply when the final commit arrives + * + * If no worker is available to handle streamed transaction, the data is + * written to temporary files and then applied at once when the final commit + * arrives. * * Unlike the regular (non-streamed) case, handling streamed transactions has * to handle aborts of both the toplevel transaction and subtransactions. This @@ -219,20 +236,7 @@ typedef struct ApplyExecutionData PartitionTupleRouting *proute; /* partition routing info */ } ApplyExecutionData; -/* Struct for saving and restoring apply errcontext information */ -typedef struct ApplyErrorCallbackArg -{ - LogicalRepMsgType command; /* 0 if invalid */ - LogicalRepRelMapEntry *rel; - - /* Remote node information */ - int remote_attnum; /* -1 if invalid */ - TransactionId remote_xid; - XLogRecPtr finish_lsn; - char *origin_name; -} ApplyErrorCallbackArg; - -static ApplyErrorCallbackArg apply_error_callback_arg = +ApplyErrorCallbackArg apply_error_callback_arg = { .command = 0, .rel = NULL, @@ -242,7 +246,7 @@ static ApplyErrorCallbackArg apply_error_callback_arg = .origin_name = NULL, }; -static MemoryContext ApplyMessageContext = NULL; +MemoryContext ApplyMessageContext = NULL; MemoryContext ApplyContext = NULL; /* per stream context for streaming transactions */ @@ -251,27 +255,38 @@ static MemoryContext LogicalStreamingContext = NULL; WalReceiverConn *LogRepWorkerWalRcvConn = NULL; Subscription *MySubscription = NULL; -static bool MySubscriptionValid = false; +bool MySubscriptionValid = false; bool in_remote_transaction = false; static XLogRecPtr remote_final_lsn = InvalidXLogRecPtr; /* fields valid only when processing streamed transaction */ -static bool in_streamed_transaction = false; +bool in_streamed_transaction = false; + +TransactionId stream_xid = InvalidTransactionId; +static ApplyBgworkerState *stream_apply_worker = NULL; + +/* check if we are applying the transaction in apply background worker */ +#define apply_bgworker_active() (in_streamed_transaction && stream_apply_worker != NULL) -static TransactionId stream_xid = InvalidTransactionId; +/* + * The number of changes during one streaming block (only for apply background + * workers) + */ +static uint32 nchanges = 0; /* * We enable skipping all data modification changes (INSERT, UPDATE, etc.) for * the subscription if the remote transaction's finish LSN matches the subskiplsn. * Once we start skipping changes, we don't stop it until we skip all changes of * the transaction even if pg_subscription is updated and MySubscription->skiplsn - * gets changed or reset during that. Also, in streaming transaction cases, we - * don't skip receiving and spooling the changes since we decide whether or not + * gets changed or reset during that. Also, in streaming transaction cases (streaming = on), + * we don't skip receiving and spooling the changes since we decide whether or not * to skip applying the changes when starting to apply changes. The subskiplsn is * cleared after successfully skipping the transaction or applying non-empty * transaction. The latter prevents the mistakenly specified subskiplsn from - * being left. + * being left. Note that we cannot skip the streaming transaction in apply mode, + * because we cannot get the finish LSN before applying the changes. */ static XLogRecPtr skip_xact_finish_lsn = InvalidXLogRecPtr; #define is_skipping_changes() (unlikely(!XLogRecPtrIsInvalid(skip_xact_finish_lsn))) @@ -324,9 +339,6 @@ static void maybe_reread_subscription(void); static void DisableSubscriptionAndExit(void); -/* prototype needed because of stream_commit */ -static void apply_dispatch(StringInfo s); - static void apply_handle_commit_internal(LogicalRepCommitData *commit_data); static void apply_handle_insert_internal(ApplyExecutionData *edata, ResultRelInfo *relinfo, @@ -359,7 +371,6 @@ static void stop_skipping_changes(void); static void clear_subscription_skip_lsn(XLogRecPtr finish_lsn); /* Functions for apply error callback */ -static void apply_error_callback(void *arg); static inline void set_apply_error_context_xact(TransactionId xid, XLogRecPtr lsn); static inline void reset_apply_error_context_info(void); @@ -426,41 +437,76 @@ end_replication_step(void) } /* - * Handle streamed transactions. + * Handle streamed transactions for both main apply worker and apply background + * worker. * - * If in streaming mode (receiving a block of streamed transaction), we - * simply redirect it to a file for the proper toplevel transaction. + * In streaming case (receiving a block of streamed transaction), for + * SUBSTREAM_ON mode, we simply redirect it to a file for the proper toplevel + * transaction, and for SUBSTREAM_APPLY mode, we send the changes to background + * apply worker (LOGICAL_REP_MSG_RELATION or LOGICAL_REP_MSG_TYPE changes will + * also be applied in main apply worker). * - * Returns true for streamed transactions, false otherwise (regular mode). + * For non-streamed transactions, returns false; + * For streamed transactions, returns true if in main apply worker (except we + * apply streamed transaction in "apply" mode and address + * LOGICAL_REP_MSG_RELATION or LOGICAL_REP_MSG_TYPE changes), false otherwise. */ static bool handle_streamed_transaction(LogicalRepMsgType action, StringInfo s) { - TransactionId xid; + TransactionId current_xid = InvalidTransactionId; /* not in streaming mode */ - if (!in_streamed_transaction) + if (!(in_streamed_transaction || am_apply_bgworker())) return false; - Assert(stream_fd != NULL); Assert(TransactionIdIsValid(stream_xid)); /* * We should have received XID of the subxact as the first part of the * message, so extract it. */ - xid = pq_getmsgint(s, 4); + current_xid = pq_getmsgint(s, 4); - if (!TransactionIdIsValid(xid)) + if (!TransactionIdIsValid(current_xid)) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg_internal("invalid transaction ID in streamed replication transaction"))); - /* Add the new subxact to the array (unless already there). */ - subxact_info_add(xid); + if (am_apply_bgworker()) + { + /* define a savepoint for a subxact if needed. */ + apply_bgworker_subxact_info_add(current_xid); - /* write the change to the current file */ - stream_write_change(action, s); + return false; + } + else if (apply_bgworker_active()) + { + /* + * If we decided to apply the changes of this transaction in an apply + * background worker, pass the data to the worker. + */ + apply_bgworker_send_data(stream_apply_worker, s->len, s->data); + nchanges += 1; + + /* + * XXX The publisher side doesn't always send relation/type update + * messages after the streaming transaction, so also update the + * relation/type in main apply worker here. See function + * cleanup_rel_sync_cache. + */ + if (action == LOGICAL_REP_MSG_RELATION || + action == LOGICAL_REP_MSG_TYPE) + return false; + } + else + { + /* Add the new subxact to the array (unless already there). */ + subxact_info_add(current_xid); + + /* write the change to the current file */ + stream_write_change(action, s); + } return true; } @@ -844,6 +890,9 @@ apply_handle_commit(StringInfo s) apply_handle_commit_internal(&commit_data); + /* Check the status of apply background worker if any. */ + apply_bgworker_check_status(); + /* Process any tables that are being synchronized in parallel. */ process_syncing_tables(commit_data.end_lsn); @@ -898,7 +947,9 @@ apply_handle_prepare_internal(LogicalRepPreparedTxnData *prepare_data) * BeginTransactionBlock is necessary to balance the EndTransactionBlock * called within the PrepareTransactionBlock below. */ - BeginTransactionBlock(); + if (!IsTransactionBlock()) + BeginTransactionBlock(); + CommitTransactionCommand(); /* Completes the preceding Begin command. */ /* @@ -950,6 +1001,9 @@ apply_handle_prepare(StringInfo s) in_remote_transaction = false; + /* Check the status of apply background worker if any. */ + apply_bgworker_check_status(); + /* Process any tables that are being synchronized in parallel. */ process_syncing_tables(prepare_data.end_lsn); @@ -1064,10 +1118,6 @@ apply_handle_rollback_prepared(StringInfo s) /* * Handle STREAM PREPARE. - * - * Logic is in two parts: - * 1. Replay all the spooled operations - * 2. Mark the transaction as prepared */ static void apply_handle_stream_prepare(StringInfo s) @@ -1088,24 +1138,70 @@ apply_handle_stream_prepare(StringInfo s) logicalrep_read_stream_prepare(s, &prepare_data); set_apply_error_context_xact(prepare_data.xid, prepare_data.prepare_lsn); - elog(DEBUG1, "received prepare for streamed transaction %u", prepare_data.xid); + if (am_apply_bgworker()) + { + /* Mark the transaction as prepared. */ + apply_handle_prepare_internal(&prepare_data); - /* Replay all the spooled operations. */ - apply_spooled_messages(prepare_data.xid, prepare_data.prepare_lsn); + CommitTransactionCommand(); - /* Mark the transaction as prepared. */ - apply_handle_prepare_internal(&prepare_data); + pgstat_report_stat(false); - CommitTransactionCommand(); + list_free(subxactlist); + subxactlist = NIL; - pgstat_report_stat(false); + apply_bgworker_set_status(APPLY_BGWORKER_FINISHED); + } + else + { + ApplyBgworkerState *wstate = apply_bgworker_find_or_start(prepare_data.xid, false); - store_flush_position(prepare_data.end_lsn); + elog(DEBUG1, "received prepare for streamed transaction %u", + prepare_data.xid); + + /* + * This is the main apply worker. Check if we are processing this + * transaction in a apply background worker. + */ + if (wstate) + { + apply_bgworker_send_data(wstate, s->len, s->data); + + apply_bgworker_wait_for(wstate, APPLY_BGWORKER_FINISHED); + apply_bgworker_free(wstate); + + pgstat_report_stat(false); + store_flush_position(prepare_data.end_lsn); + } + else + { + /* + * This is the main apply worker and the transaction has been + * serialized to file, replay all the spooled operations. + */ + apply_spooled_messages(prepare_data.xid, prepare_data.prepare_lsn); + + /* Mark the transaction as prepared. */ + apply_handle_prepare_internal(&prepare_data); + + CommitTransactionCommand(); + + pgstat_report_stat(false); + + store_flush_position(prepare_data.end_lsn); + + in_remote_transaction = false; + + /* unlink the files with serialized changes and subxact info. */ + stream_cleanup_files(MyLogicalRepWorker->subid, prepare_data.xid); + } + } in_remote_transaction = false; + stream_apply_worker = NULL; - /* unlink the files with serialized changes and subxact info. */ - stream_cleanup_files(MyLogicalRepWorker->subid, prepare_data.xid); + /* Check the status of apply background worker if any. */ + apply_bgworker_check_status(); /* Process any tables that are being synchronized in parallel. */ process_syncing_tables(prepare_data.end_lsn); @@ -1155,15 +1251,6 @@ apply_handle_stream_start(StringInfo s) (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg_internal("duplicate STREAM START message"))); - /* - * Start a transaction on stream start, this transaction will be committed - * on the stream stop unless it is a tablesync worker in which case it - * will be committed after processing all the messages. We need the - * transaction for handling the buffile, used for serializing the - * streaming data and subxact info. - */ - begin_replication_step(); - /* notify handle methods we're processing a remote transaction */ in_streamed_transaction = true; @@ -1177,36 +1264,92 @@ apply_handle_stream_start(StringInfo s) set_apply_error_context_xact(stream_xid, InvalidXLogRecPtr); + if (am_apply_bgworker()) + { + /* + * Make sure the handle apply_dispatch methods are aware we're in a + * remote transaction. + */ + in_remote_transaction = true; + + /* If we are in a apply background worker, begin the transaction */ + AcceptInvalidationMessages(); + maybe_reread_subscription(); + + StartTransactionCommand(); + BeginTransactionBlock(); + CommitTransactionCommand(); + + return; + } + /* - * Initialize the worker's stream_fileset if we haven't yet. This will be - * used for the entire duration of the worker so create it in a permanent - * context. We create this on the very first streaming message from any - * transaction and then use it for this and other streaming transactions. - * Now, we could create a fileset at the start of the worker as well but - * then we won't be sure that it will ever be used. + * This is the main apply worker. Check if there is any free apply + * background worker we can use to process this transaction. */ - if (MyLogicalRepWorker->stream_fileset == NULL) + stream_apply_worker = apply_bgworker_find_or_start(stream_xid, first_segment); + + if (stream_apply_worker) { - MemoryContext oldctx; + /* + * If we have found a free worker or if we are already applying this + * transaction in an apply background worker, then we pass the data to + * that worker. + */ + if (first_segment) + apply_bgworker_send_data(stream_apply_worker, s->len, s->data); - oldctx = MemoryContextSwitchTo(ApplyContext); + nchanges = 0; + elog(DEBUG1, "starting streaming of xid %u", stream_xid); + } - MyLogicalRepWorker->stream_fileset = palloc(sizeof(FileSet)); - FileSetInit(MyLogicalRepWorker->stream_fileset); + /* + * If no worker is available for the first stream start, we start to + * serialize all the changes of the transaction. + */ + else + { + /* + * Start a transaction on stream start, this transaction will be + * committed on the stream stop unless it is a tablesync worker in + * which case it will be committed after processing all the messages. + * We need the transaction for handling the buffile, used for + * serializing the streaming data and subxact info. + */ + begin_replication_step(); - MemoryContextSwitchTo(oldctx); - } + /* + * Initialize the worker's stream_fileset if we haven't yet. This will + * be used for the entire duration of the worker so create it in a + * permanent context. We create this on the very first streaming + * message from any transaction and then use it for this and other + * streaming transactions. Now, we could create a fileset at the start + * of the worker as well but then we won't be sure that it will ever + * be used. + */ + if (MyLogicalRepWorker->stream_fileset == NULL) + { + MemoryContext oldctx; - /* open the spool file for this transaction */ - stream_open_file(MyLogicalRepWorker->subid, stream_xid, first_segment); + oldctx = MemoryContextSwitchTo(ApplyContext); - /* if this is not the first segment, open existing subxact file */ - if (!first_segment) - subxact_info_read(MyLogicalRepWorker->subid, stream_xid); + MyLogicalRepWorker->stream_fileset = palloc(sizeof(FileSet)); + FileSetInit(MyLogicalRepWorker->stream_fileset); - pgstat_report_activity(STATE_RUNNING, NULL); + MemoryContextSwitchTo(oldctx); + } - end_replication_step(); + /* open the spool file for this transaction */ + stream_open_file(MyLogicalRepWorker->subid, stream_xid, first_segment); + + /* if this is not the first segment, open existing subxact file */ + if (!first_segment) + subxact_info_read(MyLogicalRepWorker->subid, stream_xid); + + end_replication_step(); + } + + pgstat_report_activity(STATE_RUNNING, NULL); } /* @@ -1220,44 +1363,47 @@ apply_handle_stream_stop(StringInfo s) (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg_internal("STREAM STOP message without STREAM START"))); - /* - * Close the file with serialized changes, and serialize information about - * subxacts for the toplevel transaction. - */ - subxact_info_write(MyLogicalRepWorker->subid, stream_xid); - stream_close_file(); + if (apply_bgworker_active()) + { + char action = LOGICAL_REP_MSG_STREAM_STOP; - /* We must be in a valid transaction state */ - Assert(IsTransactionState()); + apply_bgworker_send_data(stream_apply_worker, 1, &action); + apply_bgworker_wait_for(stream_apply_worker, APPLY_BGWORKER_READY); - /* Commit the per-stream transaction */ - CommitTransactionCommand(); + elog(DEBUG1, "stopped streaming of xid %u, %u changes streamed", stream_xid, nchanges); + } + else + { + /* + * Close the file with serialized changes, and serialize information + * about subxacts for the toplevel transaction. + */ + subxact_info_write(MyLogicalRepWorker->subid, stream_xid); + stream_close_file(); - in_streamed_transaction = false; + /* We must be in a valid transaction state */ + Assert(IsTransactionState()); + + /* Commit the per-stream transaction */ + CommitTransactionCommand(); + + /* Reset per-stream context */ + MemoryContextReset(LogicalStreamingContext); + } - /* Reset per-stream context */ - MemoryContextReset(LogicalStreamingContext); + in_streamed_transaction = false; + stream_apply_worker = NULL; pgstat_report_activity(STATE_IDLE, NULL); reset_apply_error_context_info(); } /* - * Handle STREAM abort message. + * Handle STREAM ABORT message when the transaction was spilled to disk. */ static void -apply_handle_stream_abort(StringInfo s) +serialize_stream_abort(TransactionId xid, TransactionId subxid) { - TransactionId xid; - TransactionId subxid; - - if (in_streamed_transaction) - ereport(ERROR, - (errcode(ERRCODE_PROTOCOL_VIOLATION), - errmsg_internal("STREAM ABORT message without STREAM STOP"))); - - logicalrep_read_stream_abort(s, &xid, &subxid); - /* * If the two XIDs are the same, it's in fact abort of toplevel xact, so * just delete the files with serialized info. @@ -1339,8 +1485,137 @@ apply_handle_stream_abort(StringInfo s) end_replication_step(); CommitTransactionCommand(); } +} - reset_apply_error_context_info(); +/* + * Handle STREAM ABORT message. + */ +static void +apply_handle_stream_abort(StringInfo s) +{ + TransactionId xid; + TransactionId subxid; + LogicalRepStreamAbortData abort_data; + bool include_abort_lsn; + + if (in_streamed_transaction) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg_internal("STREAM ABORT message without STREAM STOP"))); + + /* Check whether the publisher sends abort_lsn and abort_time. */ + if (am_apply_bgworker()) + include_abort_lsn = MyParallelState->server_version >= 150000; + + logicalrep_read_stream_abort(s, &abort_data, include_abort_lsn); + + xid = abort_data.xid; + subxid = abort_data.subxid; + + if (am_apply_bgworker()) + { + elog(DEBUG1, "[Apply BGW #%u] aborting current transaction xid=%u, subxid=%u", + MyParallelState->n, GetCurrentTransactionIdIfAny(), + GetCurrentSubTransactionId()); + + /* + * Update origin state so we can restart streaming from correct + * position in case of crash. + */ + if (include_abort_lsn) + { + replorigin_session_origin_lsn = abort_data.abort_lsn; + replorigin_session_origin_timestamp = abort_data.abort_time; + } + + /* + * If the two XIDs are the same, it's in fact abort of toplevel xact, + * so just free the subxactlist. + */ + if (subxid == xid) + { + set_apply_error_context_xact(subxid, InvalidXLogRecPtr); + + AbortCurrentTransaction(); + + EndTransactionBlock(false); + CommitTransactionCommand(); + + in_remote_transaction = false; + + list_free(subxactlist); + subxactlist = NIL; + + apply_bgworker_set_status(APPLY_BGWORKER_FINISHED); + } + else + { + /* + * OK, so it's a subxact. Rollback to the savepoint. + * + * We also need to read the subxactlist, determine the offset + * tracked for the subxact, and truncate the list. + */ + int i; + bool found = false; + char spname[MAXPGPATH]; + + set_apply_error_context_xact(subxid, InvalidXLogRecPtr); + snprintf(spname, MAXPGPATH, "savepoint_for_xid_%u", subxid); + + elog(DEBUG1, "[Apply BGW #%u] rolling back to savepoint %s", + MyParallelState->n, spname); + + for (i = list_length(subxactlist) - 1; i >= 0; i--) + { + xid = (TransactionId) list_nth_int(subxactlist, i); + if (xid == subxid) + { + found = true; + break; + } + } + + if (found) + { + RollbackToSavepoint(spname); + CommitTransactionCommand(); + subxactlist = list_truncate(subxactlist, i + 1); + } + + apply_bgworker_set_status(APPLY_BGWORKER_READY); + } + + reset_apply_error_context_info(); + } + else + { + ApplyBgworkerState *wstate = apply_bgworker_find_or_start(xid, false); + + /* + * This is the main apply worker. Check if we are processing this + * transaction in a apply background worker. + */ + if (wstate) + { + apply_bgworker_send_data(wstate, s->len, s->data); + + if (subxid == xid) + { + apply_bgworker_wait_for(wstate, APPLY_BGWORKER_FINISHED); + apply_bgworker_free(wstate); + } + else + apply_bgworker_wait_for(wstate, APPLY_BGWORKER_READY); + } + + /* + * We are in main apply worker and the transaction has been serialized + * to file. + */ + else + serialize_stream_abort(xid, subxid); + } } /* @@ -1462,40 +1737,6 @@ apply_spooled_messages(TransactionId xid, XLogRecPtr lsn) return; } -/* - * Handle STREAM COMMIT message. - */ -static void -apply_handle_stream_commit(StringInfo s) -{ - TransactionId xid; - LogicalRepCommitData commit_data; - - if (in_streamed_transaction) - ereport(ERROR, - (errcode(ERRCODE_PROTOCOL_VIOLATION), - errmsg_internal("STREAM COMMIT message without STREAM STOP"))); - - xid = logicalrep_read_stream_commit(s, &commit_data); - set_apply_error_context_xact(xid, commit_data.commit_lsn); - - elog(DEBUG1, "received commit for streamed transaction %u", xid); - - apply_spooled_messages(xid, commit_data.commit_lsn); - - apply_handle_commit_internal(&commit_data); - - /* unlink the files with serialized changes and subxact info */ - stream_cleanup_files(MyLogicalRepWorker->subid, xid); - - /* Process any tables that are being synchronized in parallel. */ - process_syncing_tables(commit_data.end_lsn); - - pgstat_report_activity(STATE_IDLE, NULL); - - reset_apply_error_context_info(); -} - /* * Helper function for apply_handle_commit and apply_handle_stream_commit. */ @@ -2445,11 +2686,105 @@ apply_handle_truncate(StringInfo s) end_replication_step(); } +/* + * Handle STREAM COMMIT message. + */ +static void +apply_handle_stream_commit(StringInfo s) +{ + LogicalRepCommitData commit_data; + TransactionId xid; + + if (in_streamed_transaction) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg_internal("STREAM COMMIT message without STREAM STOP"))); + + xid = logicalrep_read_stream_commit(s, &commit_data); + set_apply_error_context_xact(xid, commit_data.commit_lsn); + + if (am_apply_bgworker()) + { + /* + * Update origin state so we can restart streaming from correct + * position in case of crash. + */ + replorigin_session_origin_lsn = commit_data.end_lsn; + replorigin_session_origin_timestamp = commit_data.committime; + + CommitTransactionCommand(); + EndTransactionBlock(false); + CommitTransactionCommand(); + + in_remote_transaction = false; + + pgstat_report_stat(false); + + list_free(subxactlist); + subxactlist = NIL; + + apply_bgworker_set_status(APPLY_BGWORKER_FINISHED); + } + else + { + /* + * This is the main apply worker. Check if we are processing this + * transaction in an apply background worker. + */ + ApplyBgworkerState *wstate = apply_bgworker_find_or_start(xid, false); + + elog(DEBUG1, "received commit for streamed transaction %u", xid); + + if (wstate) + { + /* Send commit message */ + apply_bgworker_send_data(wstate, s->len, s->data); + + /* Wait for apply background worker to finish */ + apply_bgworker_wait_for(wstate, APPLY_BGWORKER_FINISHED); + + pgstat_report_stat(false); + store_flush_position(commit_data.end_lsn); + stop_skipping_changes(); + + apply_bgworker_free(wstate); + + /* + * The transaction is either non-empty or skipped, so we clear the + * subskiplsn. + */ + clear_subscription_skip_lsn(commit_data.commit_lsn); + } + else + { + /* + * This is the main apply worker and the transaction has been + * serialized to file, replay all the spooled operations. + */ + apply_spooled_messages(xid, commit_data.commit_lsn); + + apply_handle_commit_internal(&commit_data); + + /* unlink the files with serialized changes and subxact info */ + stream_cleanup_files(MyLogicalRepWorker->subid, xid); + } + } + + /* Check the status of apply background worker if any. */ + apply_bgworker_check_status(); + + /* Process any tables that are being synchronized in parallel. */ + process_syncing_tables(commit_data.end_lsn); + + pgstat_report_activity(STATE_IDLE, NULL); + + reset_apply_error_context_info(); +} /* * Logical replication protocol message dispatcher. */ -static void +void apply_dispatch(StringInfo s) { LogicalRepMsgType action = pq_getmsgbyte(s); @@ -2618,6 +2953,10 @@ store_flush_position(XLogRecPtr remote_lsn) { FlushPosition *flushpos; + /* We only need to collect the LSN in main apply worker */ + if (am_apply_bgworker()) + return; + /* Need to do this in permanent context */ MemoryContextSwitchTo(ApplyContext); @@ -2632,7 +2971,7 @@ store_flush_position(XLogRecPtr remote_lsn) /* Update statistics of the worker. */ -static void +void UpdateWorkerStats(XLogRecPtr last_lsn, TimestampTz send_time, bool reply) { MyLogicalRepWorker->last_lsn = last_lsn; @@ -2794,6 +3133,9 @@ LogicalRepApplyLoop(XLogRecPtr last_received) AcceptInvalidationMessages(); maybe_reread_subscription(); + /* Check the status of apply background worker if any. */ + apply_bgworker_check_status(); + /* Process any table synchronization changes. */ process_syncing_tables(last_received); } @@ -3095,7 +3437,7 @@ maybe_reread_subscription(void) /* * Callback from subscription syscache invalidation. */ -static void +void subscription_change_cb(Datum arg, int cacheid, uint32 hashvalue) { MySubscriptionValid = false; @@ -3691,7 +4033,7 @@ ApplyWorkerMain(Datum main_arg) originid = replorigin_by_name(originname, true); if (!OidIsValid(originid)) originid = replorigin_create(originname); - replorigin_session_setup(originid); + replorigin_session_setup(originid, true); replorigin_session_origin = originid; origin_startpos = replorigin_session_get_progress(false); CommitTransactionCommand(); @@ -3738,7 +4080,7 @@ ApplyWorkerMain(Datum main_arg) options.proto.logical.publication_names = MySubscription->publications; options.proto.logical.binary = MySubscription->binary; - options.proto.logical.streaming = MySubscription->stream; + options.proto.logical.streaming = (MySubscription->stream != SUBSTREAM_OFF); options.proto.logical.twophase = false; if (!am_tablesync_worker()) @@ -3896,7 +4238,8 @@ clear_subscription_skip_lsn(XLogRecPtr finish_lsn) XLogRecPtr myskiplsn = MySubscription->skiplsn; bool started_tx = false; - if (likely(XLogRecPtrIsInvalid(myskiplsn))) + if (likely(XLogRecPtrIsInvalid(myskiplsn)) || + am_apply_bgworker()) return; if (!IsTransactionState()) @@ -3968,7 +4311,7 @@ clear_subscription_skip_lsn(XLogRecPtr finish_lsn) } /* Error callback to give more context info about the change being applied */ -static void +void apply_error_callback(void *arg) { ApplyErrorCallbackArg *errarg = &apply_error_callback_arg; diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c index 8deae57143..6ebfa24baf 100644 --- a/src/backend/replication/pgoutput/pgoutput.c +++ b/src/backend/replication/pgoutput/pgoutput.c @@ -1833,7 +1833,7 @@ pgoutput_stream_abort(struct LogicalDecodingContext *ctx, Assert(rbtxn_is_streamed(toptxn)); OutputPluginPrepareWrite(ctx, true); - logicalrep_write_stream_abort(ctx->out, toptxn->xid, txn->xid); + logicalrep_write_stream_abort(ctx->out, toptxn->xid, txn, abort_lsn); OutputPluginWrite(ctx, true); cleanup_rel_sync_cache(toptxn->xid, false); diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c index 87c15b9c6f..ba781e6f08 100644 --- a/src/backend/utils/activity/wait_event.c +++ b/src/backend/utils/activity/wait_event.c @@ -388,6 +388,9 @@ pgstat_get_wait_ipc(WaitEventIPC w) case WAIT_EVENT_HASH_GROW_BUCKETS_REINSERT: event_name = "HashGrowBucketsReinsert"; break; + case WAIT_EVENT_LOGICAL_APPLY_WORKER_STATE_CHANGE: + event_name = "LogicalApplyWorkerStateChange"; + break; case WAIT_EVENT_LOGICAL_SYNC_DATA: event_name = "LogicalSyncData"; break; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 7cc9c72e49..6f8b30abb0 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -4450,7 +4450,7 @@ getSubscriptions(Archive *fout) if (fout->remoteVersion >= 140000) appendPQExpBufferStr(query, " s.substream,\n"); else - appendPQExpBufferStr(query, " false AS substream,\n"); + appendPQExpBufferStr(query, " 'f' AS substream,\n"); if (fout->remoteVersion >= 150000) appendPQExpBufferStr(query, @@ -4580,8 +4580,10 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) if (strcmp(subinfo->subbinary, "t") == 0) appendPQExpBufferStr(query, ", binary = true"); - if (strcmp(subinfo->substream, "f") != 0) + if (strcmp(subinfo->substream, "t") == 0) appendPQExpBufferStr(query, ", streaming = on"); + else if (strcmp(subinfo->substream, "a") == 0) + appendPQExpBufferStr(query, ", streaming = apply"); if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0) appendPQExpBufferStr(query, ", two_phase = on"); diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index d1260f590c..9b394a45fe 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -68,7 +68,8 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW bool subbinary; /* True if the subscription wants the * publisher to send data in binary */ - bool substream; /* Stream in-progress transactions. */ + char substream; /* Stream in-progress transactions. + * See SUBSTREAM_xxx constants. */ char subtwophasestate; /* Stream two-phase transactions */ @@ -109,7 +110,7 @@ typedef struct Subscription bool enabled; /* Indicates if the subscription is enabled */ bool binary; /* Indicates if the subscription wants data in * binary format */ - bool stream; /* Allow streaming in-progress transactions. */ + char stream; /* Allow streaming in-progress transactions. */ char twophasestate; /* Allow streaming two-phase transactions */ bool disableonerr; /* Indicates if the subscription should be * automatically disabled if a worker error @@ -120,6 +121,18 @@ typedef struct Subscription List *publications; /* List of publication names to subscribe to */ } Subscription; +/* Disallow streaming in-progress transactions */ +#define SUBSTREAM_OFF 'f' + +/* + * Streaming transactions are written to a temporary file and applied only + * after the transaction is committed on upstream. + */ +#define SUBSTREAM_ON 't' + +/* Streaming transactions are applied immediately via a background worker */ +#define SUBSTREAM_APPLY 'a' + extern Subscription *GetSubscription(Oid subid, bool missing_ok); extern void FreeSubscription(Subscription *sub); extern void DisableSubscription(Oid subid); diff --git a/src/include/replication/logicalproto.h b/src/include/replication/logicalproto.h index a771ab8ff3..7bba77c9e7 100644 --- a/src/include/replication/logicalproto.h +++ b/src/include/replication/logicalproto.h @@ -175,6 +175,17 @@ typedef struct LogicalRepRollbackPreparedTxnData char gid[GIDSIZE]; } LogicalRepRollbackPreparedTxnData; +/* + * Transaction protocol information for stream abort. + */ +typedef struct LogicalRepStreamAbortData +{ + TransactionId xid; + TransactionId subxid; + XLogRecPtr abort_lsn; + TimestampTz abort_time; +} LogicalRepStreamAbortData; + extern void logicalrep_write_begin(StringInfo out, ReorderBufferTXN *txn); extern void logicalrep_read_begin(StringInfo in, LogicalRepBeginData *begin_data); @@ -246,9 +257,11 @@ extern void logicalrep_write_stream_commit(StringInfo out, ReorderBufferTXN *txn extern TransactionId logicalrep_read_stream_commit(StringInfo out, LogicalRepCommitData *commit_data); extern void logicalrep_write_stream_abort(StringInfo out, TransactionId xid, - TransactionId subxid); -extern void logicalrep_read_stream_abort(StringInfo in, TransactionId *xid, - TransactionId *subxid); + ReorderBufferTXN *txn, + XLogRecPtr abort_lsn); +extern void logicalrep_read_stream_abort(StringInfo in, + LogicalRepStreamAbortData *abort_data, + bool include_abort_lsn); extern char *logicalrep_message_type(LogicalRepMsgType action); #endif /* LOGICAL_PROTO_H */ diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h index cd1b6e8afc..6a1af7f13c 100644 --- a/src/include/replication/logicalworker.h +++ b/src/include/replication/logicalworker.h @@ -13,6 +13,7 @@ #define LOGICALWORKER_H extern void ApplyWorkerMain(Datum main_arg); +extern void ApplyBgworkerMain(Datum main_arg); extern bool IsLogicalWorker(void); diff --git a/src/include/replication/origin.h b/src/include/replication/origin.h index 2d1b5e5c28..c7389b40a7 100644 --- a/src/include/replication/origin.h +++ b/src/include/replication/origin.h @@ -53,7 +53,7 @@ extern XLogRecPtr replorigin_get_progress(RepOriginId node, bool flush); extern void replorigin_session_advance(XLogRecPtr remote_commit, XLogRecPtr local_commit); -extern void replorigin_session_setup(RepOriginId node); +extern void replorigin_session_setup(RepOriginId node, bool must_acquire); extern void replorigin_session_reset(void); extern XLogRecPtr replorigin_session_get_progress(bool flush); diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h index 4a01f877e5..aba1640f4d 100644 --- a/src/include/replication/reorderbuffer.h +++ b/src/include/replication/reorderbuffer.h @@ -301,6 +301,7 @@ typedef struct ReorderBufferTXN { TimestampTz commit_time; TimestampTz prepare_time; + TimestampTz abort_time; } xact_time; /* @@ -647,9 +648,11 @@ extern void ReorderBufferFinishPrepared(ReorderBuffer *rb, TransactionId xid, extern void ReorderBufferAssignChild(ReorderBuffer *, TransactionId, TransactionId, XLogRecPtr commit_lsn); extern void ReorderBufferCommitChild(ReorderBuffer *, TransactionId, TransactionId, XLogRecPtr commit_lsn, XLogRecPtr end_lsn); -extern void ReorderBufferAbort(ReorderBuffer *, TransactionId, XLogRecPtr lsn); +extern void ReorderBufferAbort(ReorderBuffer *, TransactionId, XLogRecPtr lsn, + TimestampTz abort_time); extern void ReorderBufferAbortOld(ReorderBuffer *, TransactionId xid); -extern void ReorderBufferForget(ReorderBuffer *, TransactionId, XLogRecPtr lsn); +extern void ReorderBufferForget(ReorderBuffer *, TransactionId, XLogRecPtr lsn, + TimestampTz abort_time); extern void ReorderBufferInvalidate(ReorderBuffer *, TransactionId, XLogRecPtr lsn); extern void ReorderBufferSetBaseSnapshot(ReorderBuffer *, TransactionId, XLogRecPtr lsn, struct SnapshotData *snap); diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h index 901845abc2..5b9fea2c8c 100644 --- a/src/include/replication/worker_internal.h +++ b/src/include/replication/worker_internal.h @@ -17,8 +17,11 @@ #include "access/xlogdefs.h" #include "catalog/pg_subscription.h" #include "datatype/timestamp.h" +#include "replication/logicalrelation.h" #include "storage/fileset.h" #include "storage/lock.h" +#include "storage/shm_mq.h" +#include "storage/shm_toc.h" #include "storage/spin.h" @@ -60,6 +63,9 @@ typedef struct LogicalRepWorker */ FileSet *stream_fileset; + /* Indicates if this slot is used for an apply background worker. */ + bool subworker; + /* Stats. */ XLogRecPtr last_lsn; TimestampTz last_send_time; @@ -68,9 +74,70 @@ typedef struct LogicalRepWorker TimestampTz reply_time; } LogicalRepWorker; +/* Struct for saving and restoring apply errcontext information */ +typedef struct ApplyErrorCallbackArg +{ + LogicalRepMsgType command; /* 0 if invalid */ + LogicalRepRelMapEntry *rel; + + /* Remote node information */ + int remote_attnum; /* -1 if invalid */ + TransactionId remote_xid; + XLogRecPtr finish_lsn; + char *origin_name; +} ApplyErrorCallbackArg; + +/* + * Status for apply background worker. + */ +typedef enum ApplyBgworkerStatus +{ + APPLY_BGWORKER_ATTACHED = 0, + APPLY_BGWORKER_READY, + APPLY_BGWORKER_BUSY, + APPLY_BGWORKER_FINISHED, + APPLY_BGWORKER_EXIT +} ApplyBgworkerStatus; + +/* + * Shared information among apply workers. + */ +typedef struct ApplyBgworkerShared +{ + slock_t mutex; + + /* Status for apply background worker. */ + ApplyBgworkerStatus status; + + /* server version of publisher. */ + int server_version; + + TransactionId stream_xid; + uint32 n; /* id of apply background worker */ +} ApplyBgworkerShared; + +/* + * Struct for maintaining an apply background worker. + */ +typedef struct ApplyBgworkerState +{ + shm_mq_handle *mq_handle; + dsm_segment *dsm_seg; + ApplyBgworkerShared volatile *pstate; +} ApplyBgworkerState; + /* Main memory context for apply worker. Permanent during worker lifetime. */ extern PGDLLIMPORT MemoryContext ApplyContext; +extern PGDLLIMPORT MemoryContext ApplyMessageContext; + +extern PGDLLIMPORT ApplyErrorCallbackArg apply_error_callback_arg; + +extern PGDLLIMPORT bool MySubscriptionValid; + +extern PGDLLIMPORT volatile ApplyBgworkerShared *MyParallelState; +extern PGDLLIMPORT List *subxactlist; + /* libpqreceiver connection */ extern PGDLLIMPORT struct WalReceiverConn *LogRepWorkerWalRcvConn; @@ -79,13 +146,16 @@ extern PGDLLIMPORT Subscription *MySubscription; extern PGDLLIMPORT LogicalRepWorker *MyLogicalRepWorker; extern PGDLLIMPORT bool in_remote_transaction; +extern PGDLLIMPORT bool in_streamed_transaction; +extern PGDLLIMPORT TransactionId stream_xid; extern void logicalrep_worker_attach(int slot); extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid, bool only_running); extern List *logicalrep_workers_find(Oid subid, bool only_running); -extern void logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, - Oid userid, Oid relid); +extern bool logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, + Oid userid, Oid relid, + dsm_handle subworker_dsm); extern void logicalrep_worker_stop(Oid subid, Oid relid); extern void logicalrep_worker_wakeup(Oid subid, Oid relid); extern void logicalrep_worker_wakeup_ptr(LogicalRepWorker *worker); @@ -103,10 +173,39 @@ extern void process_syncing_tables(XLogRecPtr current_lsn); extern void invalidate_syncing_table_states(Datum arg, int cacheid, uint32 hashvalue); +extern void UpdateWorkerStats(XLogRecPtr last_lsn, TimestampTz send_time, + bool reply); + +/* prototype needed because of stream_commit */ +extern void apply_dispatch(StringInfo s); + +/* Function for apply error callback */ +extern void apply_error_callback(void *arg); + +extern void subscription_change_cb(Datum arg, int cacheid, uint32 hashvalue); + +/* apply background worker setup and interactions */ +extern ApplyBgworkerState *apply_bgworker_find_or_start(TransactionId xid, + bool start); +extern void apply_bgworker_wait_for(ApplyBgworkerState *wstate, + ApplyBgworkerStatus wait_for_status); +extern void apply_bgworker_send_data(ApplyBgworkerState *wstate, Size nbytes, + const void *data); +extern void apply_bgworker_free(ApplyBgworkerState *wstate); +extern void apply_bgworker_check_status(void); +extern void apply_bgworker_set_status(ApplyBgworkerStatus status); +extern void apply_bgworker_subxact_info_add(TransactionId current_xid); + static inline bool am_tablesync_worker(void) { return OidIsValid(MyLogicalRepWorker->relid); } +static inline bool +am_apply_bgworker(void) +{ + return MyLogicalRepWorker->subworker; +} + #endif /* WORKER_INTERNAL_H */ diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h index b578e2ec75..c2d2a114d7 100644 --- a/src/include/utils/wait_event.h +++ b/src/include/utils/wait_event.h @@ -105,6 +105,7 @@ typedef enum WAIT_EVENT_HASH_GROW_BUCKETS_ALLOCATE, WAIT_EVENT_HASH_GROW_BUCKETS_ELECT, WAIT_EVENT_HASH_GROW_BUCKETS_REINSERT, + WAIT_EVENT_LOGICAL_APPLY_WORKER_STATE_CHANGE, WAIT_EVENT_LOGICAL_SYNC_DATA, WAIT_EVENT_LOGICAL_SYNC_STATE_CHANGE, WAIT_EVENT_MQ_INTERNAL, diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out index 7fcfad1591..f769835af0 100644 --- a/src/test/regress/expected/subscription.out +++ b/src/test/regress/expected/subscription.out @@ -197,7 +197,7 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); DROP SUBSCRIPTION regress_testsub; -- fail - streaming must be boolean CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, streaming = foo); -ERROR: streaming requires a Boolean value +ERROR: streaming requires a Boolean value or "apply" -- now it works CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, streaming = true); WARNING: tables were not subscribed, you will have to run ALTER SUBSCRIPTION ... REFRESH PUBLICATION to subscribe the tables diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 4fb746930a..664d4b8b1a 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -116,6 +116,10 @@ Append AppendPath AppendRelInfo AppendState +ApplyBgworkerEntry +ApplyBgworkerShared +ApplyBgworkerState +ApplyBgworkerStatus ApplyErrorCallbackArg ApplyExecutionData ApplySubXactData @@ -1483,6 +1487,7 @@ LogicalRepRelId LogicalRepRelMapEntry LogicalRepRelation LogicalRepRollbackPreparedTxnData +LogicalRepStreamAbortData LogicalRepTupleData LogicalRepTyp LogicalRepWorker -- 2.23.0.windows.1