From b21087f02218b08b73281713f731c51405e92553 Mon Sep 17 00:00:00 2001 From: Zhijie Hou Date: Fri, 7 Aug 2026 17:50:02 +0800 Subject: [PATCH v23 09/12] Track dependencies for streamed transactions This commit allows tracking dependencies of streamed transactions. Regarding the streaming=on case, dependency tracking is enabled while applying spooled changes from files. In the streaming=parallel case, dependency tracking is performed when the leader sends changes to parallel workers. Apart from non-streamed transactions, the leader waits for parallel workers till the assigned transactions are finished at COMMIT/PREPARE/ABORT; thus, the XID of streamed transactions is not cached as the lastly handled one. Also, streamed transactions are not recorded as parallelized transactions because upcoming workers do not have to wait for them. Author: Zhijie Hou Author: Hayato Kuroda --- .../replication/logical/applyparallelworker.c | 8 ++- src/backend/replication/logical/worker.c | 48 +++++++++----- src/include/replication/worker_internal.h | 1 - src/test/subscription/t/050_parallel_apply.pl | 63 +++++++++++++++++++ 4 files changed, 103 insertions(+), 17 deletions(-) diff --git a/src/backend/replication/logical/applyparallelworker.c b/src/backend/replication/logical/applyparallelworker.c index 20fe780e2c0..efd1d3c177a 100644 --- a/src/backend/replication/logical/applyparallelworker.c +++ b/src/backend/replication/logical/applyparallelworker.c @@ -168,7 +168,9 @@ * key) as another ongoing transaction (see handle_dependency_on_change for * details). If so, the leader sends a list of dependent transaction IDs to the * parallel worker, indicating that the parallel apply worker must wait for - * these transactions to commit before proceeding. + * these transactions to commit before proceeding. If transactions are streamed + * but leader deciedes not to assign parallel apply workers, dependencies are + * verified when the transaction is committed. * * Tracking dependencies is necessary even when commit order is preserved. * Consider two transactions: TX-1 (INSERT row 1) and TX-2 (DELETE row 1). If @@ -1748,6 +1750,10 @@ pa_stream_abort(LogicalRepStreamAbortData *abort_data) TransactionId xid = abort_data->xid; TransactionId subxid = abort_data->subxid; + /* Streamed transactions won't be registered */ + Assert(!dshash_find(parallelized_txns, &xid, false) && + !dshash_find(parallelized_txns, &subxid, false)); + /* * Update origin state so we can restart streaming from correct position * in case of crash. diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index b3168a1df5d..6c85d498040 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -1595,13 +1595,22 @@ handle_streamed_transaction(LogicalRepMsgType action, StringInfo s) case TRANS_LEADER_SEND_TO_PARALLEL: Assert(winfo); + /* + * Pass InvalidTransactionId to skip dependency recording for this + * change. Streaming transactions are assumed not to conflict with + * other transactions, so subsequent transactions do not need to + * wait for them to finish. + */ + handle_dependency_on_change(action, s, InvalidTransactionId, winfo); + /* * XXX The publisher side doesn't always send relation/type update * messages after the streaming transaction, so also update the * relation/type in leader apply worker. See function * cleanup_rel_sync_cache. */ - if (pa_send_data(winfo, s->len, s->data)) + if (!winfo->serialize_changes && + pa_send_data(winfo, s->len, s->data)) return (action != LOGICAL_REP_MSG_RELATION && action != LOGICAL_REP_MSG_TYPE); @@ -2670,6 +2679,8 @@ apply_handle_stream_prepare(StringInfo s) apply_spooled_messages(MyLogicalRepWorker->stream_fileset, prepare_data.xid, prepare_data.prepare_lsn); + maintain_commit_order_dependency(NULL); + /* Mark the transaction as prepared. */ apply_handle_prepare_internal(&prepare_data); @@ -2693,6 +2704,12 @@ apply_handle_stream_prepare(StringInfo s) case TRANS_LEADER_SEND_TO_PARALLEL: Assert(winfo); + /* + * Build a dependency between this transaction and the lastly + * committed transaction to preserve the commit order. + */ + maintain_commit_order_dependency(winfo); + if (pa_send_data(winfo, s->len, s->data)) { /* Finish processing the streaming transaction. */ @@ -2896,14 +2913,6 @@ apply_handle_stream_start(StringInfo s) case TRANS_LEADER_SEND_TO_PARALLEL: Assert(winfo); - /* - * TODO: Support dependency tracking for streamed transactions so - * they can be applied in parallel with preceding non-streamed - * transactions. - */ - if (first_segment) - maintain_commit_order_dependency(winfo); - /* * Once we start serializing the changes, the parallel apply * worker will wait for the leader to release the stream lock @@ -3538,12 +3547,6 @@ apply_handle_stream_commit(StringInfo s) switch (apply_action) { case TRANS_LEADER_APPLY: - /* - * TODO: Support dependency tracking for streamed transactions so - * they can be applied in parallel with preceding non-streamed - * transactions. - */ - maintain_commit_order_dependency(winfo); /* * The transaction has been serialized to file, so replay all the @@ -3552,6 +3555,8 @@ apply_handle_stream_commit(StringInfo s) apply_spooled_messages(MyLogicalRepWorker->stream_fileset, xid, commit_data.commit_lsn); + maintain_commit_order_dependency(NULL); + apply_handle_commit_internal(&commit_data); /* Unlink the files with serialized changes and subxact info. */ @@ -3563,6 +3568,19 @@ apply_handle_stream_commit(StringInfo s) case TRANS_LEADER_SEND_TO_PARALLEL: Assert(winfo); + /* + * Apart from non-streaming case, no need to mark this transaction + * as parallelized. Because the leader waits until the streamed + * transaction is committed thus commit ordering is always + * preserved. + */ + + /* + * Build a dependency between this transaction and the lastly + * committed transaction to preserve the commit order. + */ + maintain_commit_order_dependency(winfo); + if (pa_send_data(winfo, s->len, s->data)) { /* Finish processing the streaming transaction. */ diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h index 699b2198ac8..5bcc5b1aa89 100644 --- a/src/include/replication/worker_internal.h +++ b/src/include/replication/worker_internal.h @@ -365,7 +365,6 @@ extern void pa_switch_to_partial_serialize(ParallelApplyWorkerInfo *winfo, extern void pa_set_xact_state(ParallelApplyWorkerShared *wshared, ParallelTransState xact_state); extern void pa_set_stream_apply_worker(ParallelApplyWorkerInfo *winfo); - extern void pa_start_subtrans(TransactionId current_xid, TransactionId top_xid); extern void pa_reset_subtrans(void); diff --git a/src/test/subscription/t/050_parallel_apply.pl b/src/test/subscription/t/050_parallel_apply.pl index 040077a84a6..527efe09913 100644 --- a/src/test/subscription/t/050_parallel_apply.pl +++ b/src/test/subscription/t/050_parallel_apply.pl @@ -493,4 +493,67 @@ $result = $node_subscriber->safe_psql('postgres', "SELECT count(1) FROM regress_tab"); is ($result, 20, 'inserts are replicated to subscriber'); +################################################## +# Test that streaming transactions respect commit order preservation when +# non-streaming transactions are being applied in parallel workers. +################################################## + +$node_publisher->append_conf('postgresql.conf', + "logical_decoding_work_mem = 64kB"); +$node_publisher->reload; + +# Truncate the data for upcoming tests +$node_publisher->safe_psql('postgres', "TRUNCATE TABLE regress_tab;"); +$node_publisher->wait_for_catchup('regress_sub'); + +# Attach the injection_point again +$node_subscriber->safe_psql('postgres', + "SELECT injection_points_attach('parallel-worker-before-commit','wait');" +); + +$node_publisher->safe_psql('postgres', + "INSERT INTO regress_tab VALUES (generate_series(71, 80), 'test');"); + +# Wait until the parallel worker enters the injection point. +$node_subscriber->wait_for_event('logical replication parallel worker', + 'parallel-worker-before-commit'); + +# Run a transaction which would be streamed +my $h = $node_publisher->background_psql('postgres', on_error_stop => 0); + +$offset = -s $node_subscriber->logfile; + +$h->query_safe( + q{ +BEGIN; +UPDATE regress_tab SET value = 'streamed-updated' WHERE id BETWEEN 71 AND 80; +INSERT INTO regress_tab VALUES (generate_series(100, 5100), 'streamed'); +}); + +# Verify the dependency is detected for the delete +$str = $node_subscriber->wait_for_log(qr/found conflicting replica identity change on table [1-9][0-9]+ from ([1-9][0-9]+)/, $offset); +$xid = $str =~ /found conflicting replica identity change on table [1-9][0-9]+ from ([1-9][0-9]+)/; + +# Verify the parallel worker waits for the same transaction +$node_subscriber->wait_for_log(qr/wait for depended xid $xid/, $offset); + +ok(1, "replica identity dependency from streamed txn detected for parallel apply"); + +# Wakeup the parallel worker +$node_subscriber->safe_psql('postgres', qq[ + SELECT injection_points_detach('parallel-worker-before-commit'); + SELECT injection_points_wakeup('parallel-worker-before-commit'); +]); + +# Verify the streamed transaction can be applied +$node_subscriber->wait_for_log(qr/finish waiting for depended xid $xid/, $offset); + +$h->query_safe("COMMIT;"); + +$node_publisher->wait_for_catchup('regress_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(1) FROM regress_tab"); +is ($result, 5011, 'inserts are replicated to subscriber'); + done_testing(); -- 2.43.0