From b0de783f23866490b5cd553499178e31457dea7c Mon Sep 17 00:00:00 2001 From: Yugabyte CI Date: Fri, 19 Jul 2024 15:00:10 -0700 Subject: [PATCH] [BACKPORT pg15-cherrypicks] all: Bulk port from master - 59 Summary: a64fbfc522 [#23120] YSQL: Do not wait for a safe snapshot in serializable read only deferrable mode. 9f82c017b5 [#23188] DocDB: Persist new colocated_id mapping discovered as part of processing CHANGE_METADATA_OP in xCluster ClusterConfig. 152252289b Update third-party dependencies to remove CentOS 7 cd7410c878 2.18.1.1 release notes (#23241) 550458d8f8 [#23047] docdb: Fix cotable ids in flushed frontier at restore 225ddfefed [PLAT-14700] Make node-agent error message on installation to be more precise 9c9a0594be [PLAT-14563] Fixing disk mounting logic to use mount points defined in the config 84eefbdb17 [DOC-368] Azure workload identity docs changes (#22881) 781af0df5d [#23183] xCluster: Move Setup, Bootstrap, Alter and Delete Target Replication functions out of CatalogManager 1d646b1217 [docs] Add ysql_output_buffer_size to yb-tserver config reference. (#23233) 2d95dd274a Fix the issue yaml parsing dd9e85b79b [PLAT-14534]Add regex match for GCP Instance template d3bba1870a update diagram (#23245) 78b317c3a1 [/PLAT-14708] Fix JSON field name in TaskInfo query ac9164b6c0 [#23173] DocDB: Allow large bytes to be passed to RateLimiter Test Plan: Jenkins: rebase: pg15-cherrypicks Reviewers: jason, tfoucher Tags: #jenkins-ready Differential Revision: https://phorge.dev.yugabyte.com/D36723 --- build-support/thirdparty_archives.yml | 71 +- .../cdc-logical-replication.md | 16 +- .../reference/configuration/yb-tserver.md | 8 + .../preview/releases/yba-releases/v2.18.md | 25 + .../preview/releases/ybdb-releases/v2.18.md | 41 + .../configure-yugabyte-platform/aws.md | 2 +- .../configure-yugabyte-platform/azure.md | 34 +- .../configure-yugabyte-platform/gcp.md | 2 +- .../cloud-permissions-nodes-aws.md | 6 +- .../cloud-permissions-nodes-azure.md | 34 +- .../cloud-permissions-nodes-gcp.md | 4 +- .../reference/configuration/yb-tserver.md | 8 + .../configure-yugabyte-platform/aws.md | 2 +- .../configure-yugabyte-platform/azure.md | 34 +- .../configure-yugabyte-platform/gcp.md | 2 +- .../cloud-permissions-nodes-aws.md | 6 +- .../cloud-permissions-nodes-azure.md | 36 +- .../cloud-permissions-nodes-gcp.md | 4 +- .../reference/configuration/yb-tserver.md | 8 + docs/data/currentVersions.json | 4 +- docs/netlify.toml | 2 +- .../cdc-logical-replication-architecture.png | Bin 0 -> 163836 bytes .../logical_replication_architecture.png | Bin 121939 -> 0 bytes managed/node-agent/cli/node/configure.go | 4 +- .../resources/node-agent-provision.yaml | 3 +- .../ynp/commands/provision_command.py | 1 - .../resources/ynp/configs/config.j2 | 3 +- .../configure_os/templates/precheck.j2 | 21 +- .../provision/configure_os/templates/run.j2 | 62 - .../provision/node_agent/node_agent.py | 6 +- managed/node-agent/util/certs_util.go | 2 +- .../java/com/yugabyte/yw/models/TaskInfo.java | 8 +- .../validators/GCPProviderValidator.java | 9 + python/yugabyte/thirdparty_tool.py | 15 +- .../src/backend/storage/lmgr/predicate.c | 24 +- .../modify-transaction-characteristics.out | 84 +- src/yb/docdb/consensus_frontier.cc | 14 + src/yb/docdb/consensus_frontier.h | 4 + src/yb/docdb/docdb_rocksdb_util.cc | 16 +- src/yb/docdb/docdb_rocksdb_util.h | 7 +- .../xcluster/xcluster-test.cc | 2 - .../xcluster/xcluster_test_base.cc | 2 +- .../xcluster/xcluster_ysql_colocated-test.cc | 44 +- src/yb/master/CMakeLists.txt | 3 + src/yb/master/catalog_manager.cc | 17 + src/yb/master/catalog_manager.h | 266 +- src/yb/master/master_replication_service.cc | 12 +- .../add_table_to_xcluster_target_task.cc | 5 +- .../xcluster/xcluster_bootstrap_helper.cc | 461 +++ .../xcluster/xcluster_bootstrap_helper.h | 116 + src/yb/master/xcluster/xcluster_manager.cc | 99 + src/yb/master/xcluster/xcluster_manager.h | 27 +- src/yb/master/xcluster/xcluster_manager_if.h | 7 + .../xcluster/xcluster_replication_group.cc | 227 +- .../xcluster/xcluster_replication_group.h | 19 +- .../xcluster/xcluster_target_manager.cc | 90 +- .../master/xcluster/xcluster_target_manager.h | 25 + ...uster_universe_replication_alter_helper.cc | 318 ++ ...luster_universe_replication_alter_helper.h | 71 + ...uster_universe_replication_setup_helper.cc | 1380 ++++++++ ...luster_universe_replication_setup_helper.h | 189 ++ src/yb/master/xrepl_catalog_manager.cc | 2831 ++--------------- src/yb/rocksdb/util/rate_limiter.cc | 12 + src/yb/rocksdb/util/rate_limiter.h | 5 +- src/yb/rocksdb/util/rate_limiter_test.cc | 27 +- src/yb/tablet/tablet_metadata.cc | 21 +- src/yb/tablet/tablet_metadata.h | 2 + src/yb/tablet/tablet_snapshots.cc | 61 +- src/yb/tablet/tablet_snapshots.h | 5 +- src/yb/tools/data-patcher.cc | 2 +- .../yb-backup/yb-backup-cross-feature-test.cc | 4 +- 71 files changed, 3833 insertions(+), 3149 deletions(-) create mode 100644 docs/static/images/architecture/cdc-logical-replication-architecture.png delete mode 100644 docs/static/images/architecture/logical_replication_architecture.png create mode 100644 src/yb/master/xcluster/xcluster_bootstrap_helper.cc create mode 100644 src/yb/master/xcluster/xcluster_bootstrap_helper.h create mode 100644 src/yb/master/xcluster/xcluster_universe_replication_alter_helper.cc create mode 100644 src/yb/master/xcluster/xcluster_universe_replication_alter_helper.h create mode 100644 src/yb/master/xcluster/xcluster_universe_replication_setup_helper.cc create mode 100644 src/yb/master/xcluster/xcluster_universe_replication_setup_helper.h diff --git a/build-support/thirdparty_archives.yml b/build-support/thirdparty_archives.yml index bca1e6594407..2d202b7cc1ef 100644 --- a/build-support/thirdparty_archives.yml +++ b/build-support/thirdparty_archives.yml @@ -1,90 +1,101 @@ -sha: 31776c4936a67fe6f1fc218fb64a6a9909c77311 +sha: b6b07342fdfd4a65ee2608d75dd31e4b0ecc0737 archives: - os_type: almalinux8 architecture: x86_64 compiler_type: clang17 - tag: v20240620163555-31776c4936-almalinux8-x86_64-clang17 + tag: v20240713003527-b6b07342fd-almalinux8-x86_64-clang17 + + - os_type: almalinux8 + architecture: x86_64 + compiler_type: clang18 + tag: v20240713003521-b6b07342fd-almalinux8-x86_64-clang18 - os_type: almalinux8 architecture: x86_64 compiler_type: gcc11 - tag: v20240620163541-31776c4936-almalinux8-x86_64-gcc11 + tag: v20240713003520-b6b07342fd-almalinux8-x86_64-gcc11 - os_type: almalinux9 architecture: x86_64 compiler_type: clang17 - tag: v20240620163555-31776c4936-almalinux9-x86_64-clang17 + tag: v20240713003540-b6b07342fd-almalinux9-x86_64-clang17 - os_type: almalinux9 architecture: x86_64 compiler_type: gcc12 - tag: v20240620163623-31776c4936-almalinux9-x86_64-gcc12 + tag: v20240713003537-b6b07342fd-almalinux9-x86_64-gcc12 - - os_type: centos7 + - os_type: amzn2 architecture: aarch64 - compiler_type: clang16 - tag: v20240620163739-31776c4936-centos7-aarch64-clang16 + compiler_type: clang17 + tag: v20240713003725-b6b07342fd-amzn2-aarch64-clang17 - - os_type: centos7 + - os_type: amzn2 architecture: aarch64 - compiler_type: clang16 + compiler_type: clang17 lto_type: full - tag: v20240620163735-31776c4936-centos7-aarch64-clang16-full-lto + tag: v20240713003827-b6b07342fd-amzn2-aarch64-clang17-full-lto - - os_type: centos7 + - os_type: amzn2 architecture: aarch64 - compiler_type: clang17 - tag: v20240620163738-31776c4936-centos7-aarch64-clang17 + compiler_type: clang18 + tag: v20240713003831-b6b07342fd-amzn2-aarch64-clang18 - - os_type: centos7 + - os_type: amzn2 architecture: aarch64 - compiler_type: clang17 + compiler_type: clang18 lto_type: full - tag: v20240620163737-31776c4936-centos7-aarch64-clang17-full-lto + tag: v20240713003853-b6b07342fd-amzn2-aarch64-clang18-full-lto - - os_type: centos7 + - os_type: amzn2 architecture: x86_64 compiler_type: clang17 - tag: v20240620163551-31776c4936-centos7-x86_64-clang17 + tag: v20240713003538-b6b07342fd-amzn2-x86_64-clang17 - - os_type: centos7 + - os_type: amzn2 architecture: x86_64 compiler_type: clang17 lto_type: full - tag: v20240620163542-31776c4936-centos7-x86_64-clang17-full-lto + tag: v20240713003542-b6b07342fd-amzn2-x86_64-clang17-full-lto - - os_type: centos7 + - os_type: amzn2 architecture: x86_64 - compiler_type: gcc11 - tag: v20240620163544-31776c4936-centos7-x86_64-gcc11 + compiler_type: clang18 + tag: v20240713003544-b6b07342fd-amzn2-x86_64-clang18 + + - os_type: amzn2 + architecture: x86_64 + compiler_type: clang18 + lto_type: full + tag: v20240713003540-b6b07342fd-amzn2-x86_64-clang18-full-lto - os_type: macos architecture: arm64 compiler_type: clang - tag: v20240620173126-31776c4936-macos-arm64 + tag: v20240713011052-b6b07342fd-macos-arm64 - os_type: macos architecture: x86_64 compiler_type: clang - tag: v20240620163640-31776c4936-macos-x86_64 + tag: v20240713003540-b6b07342fd-macos-x86_64 - os_type: ubuntu20.04 architecture: x86_64 compiler_type: clang16 - tag: v20240620163547-31776c4936-ubuntu2004-x86_64-clang16 + tag: v20240713003520-b6b07342fd-ubuntu2004-x86_64-clang16 - os_type: ubuntu22.04 architecture: x86_64 compiler_type: clang17 - tag: v20240620163538-31776c4936-ubuntu2204-x86_64-clang17 + tag: v20240713003517-b6b07342fd-ubuntu2204-x86_64-clang17 - os_type: ubuntu22.04 architecture: x86_64 compiler_type: gcc11 - tag: v20240620163535-31776c4936-ubuntu2204-x86_64-gcc11 + tag: v20240713003527-b6b07342fd-ubuntu2204-x86_64-gcc11 - os_type: ubuntu23.04 architecture: x86_64 compiler_type: gcc13 - tag: v20240620163558-31776c4936-ubuntu2304-x86_64-gcc13 + tag: v20240713003516-b6b07342fd-ubuntu2304-x86_64-gcc13 diff --git a/docs/content/preview/architecture/docdb-replication/cdc-logical-replication.md b/docs/content/preview/architecture/docdb-replication/cdc-logical-replication.md index a70e352c2d9f..c894824bdd26 100644 --- a/docs/content/preview/architecture/docdb-replication/cdc-logical-replication.md +++ b/docs/content/preview/architecture/docdb-replication/cdc-logical-replication.md @@ -19,9 +19,9 @@ CDC in YugabyteDB is based on the PostgreSQL Logical Replication model. The fund ## Architecture -![Logical-Replication-Architecture](/images/architecture/logical_replication_architecture.png) +![Logical replication architecture](/images/architecture/cdc-logical-replication-architecture.png) -The following are the main components of the Yugabyte CDC solution - +The following are the main components of the Yugabyte CDC solution: 1. Walsender - A special purpose PG backend responsible for streaming changes to the client and handling acknowledgments. @@ -41,7 +41,7 @@ The initial snapshot data for each table is consumed by executing a correspondin First, a `SET LOCAL yb_read_time TO ' ht'` command should be executed on the connection (session). The SELECT statement corresponding to the snapshot query should then be executed as part of the same transaction. -The HybridTime value to use in the `SET LOCAL yb_read_time `command is the value of the `snapshot_name` field that is returned by the `CREATE_REPLICATION_SLOT` command. Alternatively, it can be obtained by querying the `pg_replication_slots` view. +The HybridTime value to use in the `SET LOCAL yb_read_time` command is the value of the `snapshot_name` field that is returned by the `CREATE_REPLICATION_SLOT` command. Alternatively, it can be obtained by querying the `pg_replication_slots` view. During Snapshot consumption, the snapshot data from all tables will be from the same consistent state (`consistent_point`). At the end of Snapshot consumption, the state of the target system is at/based on the `consistent_point`. History of the tables as of the `consistent_point` is retained on the source until the snapshot is consumed. @@ -67,14 +67,16 @@ VWAL collects changes across multiple tablets, assembles the transactions, assig Walsender sends changes to the output plugin, which filters them according to the slot's publication and converts them into the client's desired format. These changes are then streamed to the client using the appropriate streaming replication protocols determined by the output plugin. Yugabyte follows the same streaming replication protocols as defined in PostgreSQL. + -Refer to [Replication Protocol](../../../explore/logical-replication/#Streaming-Protocol) for more details. +Refer to [Replication Protocol](../../../explore/change-data-capture/using-logical-replication/#streaming-protocol) for more details. {{< /note >}} {{< tip title="Explore" >}} - -See [Getting Started with Logical Replication](../../../explore/logical-replication/getting-started) to set up Logical Replication in YugabyteDB. + +See [Getting Started with Logical Replication](../../../explore/change-data-capture/using-logical-replication/getting-started/) to set up Logical Replication in YugabyteDB. {{< /tip >}} +--> diff --git a/docs/content/preview/reference/configuration/yb-tserver.md b/docs/content/preview/reference/configuration/yb-tserver.md index fff0e4835bca..e1b2381d05cd 100644 --- a/docs/content/preview/reference/configuration/yb-tserver.md +++ b/docs/content/preview/reference/configuration/yb-tserver.md @@ -855,6 +855,14 @@ Default: `-1` (disables logging statement durations) Specifies the lowest YSQL message level to log. +##### --ysql_output_buffer_size + +Size of YSQL layer output buffer, in bytes. YSQL buffers query responses in this output buffer until either a buffer flush is requested by the client or the buffer overflows. + +As long as no data has been flushed from the buffer, the database can retry queries on retryable errors. For example, you can increase the size of the buffer so that YSQL can retry [read restart errors](../../../architecture/transactions/read-restart-error). + +Default: `262144` (256kB, type: int32) + ### YCQL The following flags support the use of the [YCQL API](../../../api/ycql/): diff --git a/docs/content/preview/releases/yba-releases/v2.18.md b/docs/content/preview/releases/yba-releases/v2.18.md index baa8f0faadcd..d8476c216a96 100644 --- a/docs/content/preview/releases/yba-releases/v2.18.md +++ b/docs/content/preview/releases/yba-releases/v2.18.md @@ -33,6 +33,31 @@ What follows are the release notes for all releases in the **YugabyteDB Anywhere For an RSS feed of all release series to track the latest product updates, point your feed reader to the [RSS feed for releases](../index.xml). +## v2.18.8.1 - July 18, 2024 {#v2.18.8.1} + +**Build:** `2.18.8.1-b3` + +**Third-party licenses:** [YugabyteDB](https://downloads.yugabyte.com/releases/2.18.8.1/yugabytedb-2.18.8.1-b3-third-party-licenses.html), [YugabyteDB Anywhere](https://downloads.yugabyte.com/releases/2.18.8.1/yugabytedb-anywhere-2.18.8.1-b3-third-party-licenses.html) + +### Download + + + +For instructions on installing YugabyteDB Anywhere, refer to [Install YugabyteDB Anywhere](../../../yugabyte-platform/install-yugabyte-platform/). + +### Bug fixes + +#### Other + +* Repairs build failure in CentOS 7 pex/yugabundle builder Docker image. PLAT-14543 + ## v2.18.8.0 - June 21, 2024 {#v2.18.8.0} **Build:** `2.18.8.0-b42` diff --git a/docs/content/preview/releases/ybdb-releases/v2.18.md b/docs/content/preview/releases/ybdb-releases/v2.18.md index 36f8aee3308c..cb8c97dfcbbc 100644 --- a/docs/content/preview/releases/ybdb-releases/v2.18.md +++ b/docs/content/preview/releases/ybdb-releases/v2.18.md @@ -33,6 +33,47 @@ What follows are the release notes for the YugabyteDB v2.18 release series. Cont For an RSS feed of all release series to track the latest product updates, point your feed reader to the [RSS feed for releases](../index.xml). +## v2.18.8.1 - July 18, 2024 {#v2.18.8.1} + +**Build:** `2.18.8.1-b3` + +**Third-party licenses:** [YugabyteDB](https://downloads.yugabyte.com/releases/2.18.8.1/yugabytedb-2.18.8.1-b3-third-party-licenses.html), [YugabyteDB Anywhere](https://downloads.yugabyte.com/releases/2.18.8.1/yugabytedb-anywhere-2.18.8.1-b3-third-party-licenses.html) + +### Downloads + + + +### Docker + +```sh +docker pull yugabytedb/yugabyte:2.18.8.1-b3 +``` + +### Improvements + +#### DocDB + +* Allows asynchronous DNS cache updating and resolution retry upon failure to reduce RPC call delays and prevent unexpected leadership changes. {{}},{{}} + ## v2.18.8.0 - June 21, 2024 {#v2.18.8.0} **Build:** `2.18.8.0-b42` diff --git a/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/aws.md b/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/aws.md index c652473698c7..8b6e9874224f 100644 --- a/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/aws.md +++ b/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/aws.md @@ -101,7 +101,7 @@ Enter a Provider name. The Provider name is an internal tag used for organizing **Credential Type**. YBA requires the ability to create VMs in AWS. To do this, you can do one of the following: -- **Specify Access ID and Secret Key** - Create an AWS Service Account with the required permissions (refer to [Cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes/)), and provide your AWS Access Key ID and Secret Access Key. +- **Specify Access ID and Secret Key** - Create an AWS Service Account with the required permissions (refer to [Cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes-aws/)), and provide your AWS Access Key ID and Secret Access Key. - **Use IAM Role from this YBA host's instance** - Provision the YBA VM instance with an IAM role that has sufficient permissions by attaching an [IAM role](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) to the YBA VM in the **EC2** tab. This option is only available if YBA is installed on AWS. **Use AWS Route 53 DNS Server**. Choose whether to use the cloud DNS Server / load balancer for universes deployed using this provider. Generally, SQL clients should prefer to use [smart client drivers](../../../drivers-orms/smart-drivers/) to connect to cluster nodes, rather than load balancers. However, in some cases (for example, if no smart driver is available in the language), you may use a DNS Server or load-balancer. The DNS Server acts as a load-balancer that routes clients to various nodes in the database universe. YBA integrates with [Amazon Route53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html) to provide managed Canonical Name (CNAME) entries for your YugabyteDB universes, and automatically updates the DNS entry as nodes get created, removed, or undergo maintenance. diff --git a/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/azure.md b/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/azure.md index 78f8ab20a5d3..edc806bd49a0 100644 --- a/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/azure.md +++ b/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/azure.md @@ -51,13 +51,13 @@ When deploying a universe, YBA uses the provider configuration settings to do th ## Prerequisites -You need to add the following Azure cloud provider credentials via YBA: +You need to add the following Azure cloud provider credentials: +- Application client ID and (if using credentials) client secret +- Resource group name - Subscription ID - Tenant ID - SSH port and user -- Application client ID and secret -- Resource group YBA uses the credentials to automatically provision and deprovision YugabyteDB instances. @@ -107,11 +107,29 @@ Enter a Provider name. The Provider name is an internal tag used for organizing ### Cloud Info -- **Client ID** represents the [ID of an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret) registered in your Azure Active Directory. -- **Client Secret** represents the secret of an application registered in your Azure Active Directory. You need to enter the `Value` of the secret (not the `Secret ID`). -- **Resource Group** represents the group in which YugabyteDB nodes compute and network resources are created. Your Azure Active Directory application (client ID and client secret) needs to have `Network Contributor` and `Virtual Machine Contributor` roles assigned for this resource group. -- **Subscription ID** is required for cost management. The virtual machine resources managed by YBA are tagged with this subscription. -- **Tenant ID** represents the Azure Active Directory tenant ID which belongs to an active subscription. To find your tenant ID, follow instructions provided in [How to find your Azure Active Directory tenant ID](https://learn.microsoft.com/en-us/azure/active-directory/fundamentals/how-to-find-tenant). +Enter the following details of your Azure cloud account, as described in [Azure cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes-azure/). + +#### Client ID + +Provide the ID of the application you registered with the Microsoft Identity Platform. + +#### Credential type + +If you [added credentials](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=client-secret#add-credentials) in the form of a client secret to your registered application: + +1. Select **Specify Client Secret**. +1. Enter the Client Secret of the application associated with the Client ID you provided. You need to enter the `Value` of the secret (not the `Secret ID`). + +If you are using the [managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm) of the Azure VM hosting YugabyteDB Anywhere to authenticate: + +- Select **Use Managed Identity from this YBA host's instance**. + +#### Additional fields + +- **Resource Group** is the name of the resource group you created for your application, and in which YugabyteDB node compute and network resources will be created. +- **Subscription ID** is required for cost management. The virtual machine resources managed by YBA are tagged with this subscription. To get the subscription ID, open Subscriptions in Azure portal and find your subscription. Then, copy the Subscription ID. +- Optionally, if you created a different resource group for your network interfaces, provide the **Network Resource Group** name and the associated **Network Subscription ID**. If you do not provide a Network Resource Group or Subscription ID, network resources will be created in the default resource group. +- **Tenant ID** represents the tenant ID which belongs to an active subscription. To find your tenant ID, follow instructions provided in [How to find your Microsoft Entra tenant ID](https://learn.microsoft.com/en-us/entra/fundamentals/how-to-find-tenant). - **Private DNS zone** lets you use a custom domain name for the nodes in your universe. For details and instructions, see [Define a private DNS zone](#define-a-private-dns-zone). ### Regions diff --git a/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/gcp.md b/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/gcp.md index 9b9fc45bf950..c421e72c24e0 100644 --- a/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/gcp.md +++ b/docs/content/preview/yugabyte-platform/configure-yugabyte-platform/gcp.md @@ -110,7 +110,7 @@ Enter a Provider name. The Provider name is an internal tag used for organizing ### Cloud Info -If your YBA instance is not running inside GCP, you need to supply YBA with credentials to the desired GCP project by uploading a configuration file. To do this, set **Credential Type** to **Upload Service Account config** and proceed to upload the JSON file that you obtained when you created your service account, as described in [Cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes/). +If your YBA instance is not running inside GCP, you need to supply YBA with credentials to the desired GCP project by uploading a configuration file. To do this, set **Credential Type** to **Upload Service Account config** and proceed to upload the JSON file that you obtained when you created your service account, as described in [Cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes-gcp/). If your YBA instance is running inside GCP, the preferred method for authentication to the GCP APIs is to add a service account role to the GCP instance running YBA and then configure YBA to use the instance's service account. To do this, set **Credential Type** to **Use service account from this YBA host's instance**. diff --git a/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-aws.md b/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-aws.md index 972439ea0698..af8182a0bb25 100644 --- a/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-aws.md +++ b/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-aws.md @@ -123,8 +123,8 @@ If using a service account, record the following two pieces of information about | Save for later | To configure | | :--- | :--- | -| Access key ID | [AWS cloud provider](../../../configure-yugabyte-platform/aws/) | -| Secret Access Key | [AWS cloud provider](../../../configure-yugabyte-platform/aws/) | +| Access key ID | [AWS provider configuration](../../../configure-yugabyte-platform/aws/) | +| Secret Access Key | | ### IAM role @@ -178,4 +178,4 @@ If you will be using your own custom SSH keys, then ensure that you have them wh | Save for later | To configure | | :--- | :--- | -| Custom SSH keys | [AWS provider](../../../configure-yugabyte-platform/kubernetes/) | +| Custom SSH keys | [AWS provider configuration](../../../configure-yugabyte-platform/kubernetes/) | diff --git a/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-azure.md b/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-azure.md index 6ca1477ddcea..1bf892f14bc0 100644 --- a/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-azure.md +++ b/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-azure.md @@ -52,30 +52,46 @@ The more permissions that you can provide, the more YBA can automate. ## Azure -The following permissions are required for the Azure resource group where you will deploy. +### Application and resource group + +YugabyteDB Anywhere requires cloud permissions to create VMs. You grant YugabyteDB Anywhere access to manage Azure resources such as VMs by registering an application in the Azure portal so the Microsoft identity platform can provide authentication and authorization services for your application. Registering your application establishes a trust relationship between your application and the Microsoft identity platform. + +In addition, your Azure application needs to have a [resource group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/overview#resource-groups) with the following permissions: ```sh Network Contributor Virtual Machine Contributor ``` -To grant the required access, you can do one of the following: +You can optionally create a resource group for network resources if you want network interfaces to be created separately. The network resource group must have the `Network Contributor` permission. + +For more information on registering applications, refer to [Register an application with the Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=certificate) in the Microsoft Entra documentation. + +For more information on roles, refer to [Assign Azure roles using the Azure portal](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=delegate-condition) in the Microsoft Azure documentation. + +### Credentials + +YugabyteDB Anywhere can authenticate with Azure using one of the following methods: + +- [Add credentials](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=client-secret#add-credentials), in the form of a client secret, to your registered application. -- [Register an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) in the Azure portal so the Microsoft identity platform can provide authentication and authorization services for your application. Registering your application establishes a trust relationship between your application and the Microsoft identity platform. + For information on creating client secrets, see [Create a new client secret](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#option-3-create-a-new-client-secret) in the Microsoft Entra documentation. -- [Assign a managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm) to the Azure VM hosting YugabyteDB Anywhere. +- [Assign a managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm) to the Azure VM hosting YugabyteDB Anywhere. Azure will use the managed identity assigned to your instance to authenticate. -For information on assigning roles to applications, see [Assign a role to an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#assign-a-role-to-the-application); and assigning roles for managed identities, see [Assign Azure roles using the Azure portal](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=delegate-condition) in the Microsoft Azure documentation. + For information on assigning roles for managed identities, see [Assign Azure roles using the Azure portal](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=delegate-condition) in the Microsoft Azure documentation. -If you are registering an application, record the following information about your service account. You will need to provide this information later to YBA. +Record the following information about your service account. You will need to provide this information later when creating an Azure provider configuration. | Save for later | To configure | | :--- | :--- | -| **Service account details** | [Azure cloud provider](../../../configure-yugabyte-platform/azure/) | +| **Service account details** | [Azure provider configuration](../../../configure-yugabyte-platform/azure/) | | Client ID: | | -| Client Secret: | | +| Client Secret:
(not required when using managed identity) | | | Resource Group: | | | Subscription ID: | | +| (Optional) Network Resource Group: | | +| (Optional) Network Subscription ID: | | | Tenant ID: | | ## Managing SSH keys for VMs @@ -89,4 +105,4 @@ If you will be using your own custom SSH keys, then ensure that you have them wh | Save for later | To configure | | :--- | :--- | -| Custom SSH keys | [Azure provider](../../../configure-yugabyte-platform/azure/) | +| Custom SSH keys | [Azure provider configuration](../../../configure-yugabyte-platform/azure/) | diff --git a/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-gcp.md b/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-gcp.md index 502d178bc1d2..a54c68f08097 100644 --- a/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-gcp.md +++ b/docs/content/preview/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-gcp.md @@ -70,7 +70,7 @@ Then use one of the following methods: | Save for later | To configure | | :--- | :--- | -| Service account JSON | [GCP cloud provider](../../../configure-yugabyte-platform/gcp/) | +| Service account JSON | [GCP provider configuration](../../../configure-yugabyte-platform/gcp/) | ## Managing SSH keys for VMs @@ -83,4 +83,4 @@ If you will be using your own custom SSH keys, then ensure that you have them wh | Save for later | To configure | | :--- | :--- | -| Custom SSH keys | [GCP provider](../../../configure-yugabyte-platform/gcp/) | +| Custom SSH keys | [GCP provider configuration](../../../configure-yugabyte-platform/gcp/) | diff --git a/docs/content/stable/reference/configuration/yb-tserver.md b/docs/content/stable/reference/configuration/yb-tserver.md index f4c285516396..15ad7966c670 100644 --- a/docs/content/stable/reference/configuration/yb-tserver.md +++ b/docs/content/stable/reference/configuration/yb-tserver.md @@ -855,6 +855,14 @@ Default: `-1` (disables logging statement durations) Specifies the lowest YSQL message level to log. +##### --ysql_output_buffer_size + +Size of YSQL layer output buffer, in bytes. YSQL buffers query responses in this output buffer until either a buffer flush is requested by the client or the buffer overflows. + +As long as no data has been flushed from the buffer, the database can retry queries on retryable errors. For example, you can increase the size of the buffer so that YSQL can retry [read restart errors](../../../architecture/transactions/read-restart-error). + +Default: `262144` (256kB, type: int32) + ### YCQL The following flags support the use of the [YCQL API](../../../api/ycql/): diff --git a/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/aws.md b/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/aws.md index 3a56d91f9a0f..ce3499b4cbf2 100644 --- a/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/aws.md +++ b/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/aws.md @@ -98,7 +98,7 @@ Enter a Provider name. The Provider name is an internal tag used for organizing **Credential Type**. YBA requires the ability to create VMs in AWS. To do this, you can do one of the following: -- **Specify Access ID and Secret Key** - Create an AWS Service Account with the required permissions (refer to [Cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes/)), and provide your AWS Access Key ID and Secret Access Key. +- **Specify Access ID and Secret Key** - Create an AWS Service Account with the required permissions (refer to [Cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes-aws/)), and provide your AWS Access Key ID and Secret Access Key. - **Use IAM Role from this YBA host's instance** - Provision the YBA VM instance with an IAM role that has sufficient permissions by attaching an [IAM role](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) to the YBA VM in the **EC2** tab. This option is only available if YBA is installed on AWS. **Use AWS Route 53 DNS Server**. Choose whether to use the cloud DNS Server / load balancer for universes deployed using this provider. Generally, SQL clients should prefer to use [smart client drivers](../../../drivers-orms/smart-drivers/) to connect to cluster nodes, rather than load balancers. However, in some cases (for example, if no smart driver is available in the language), you may use a DNS Server or load-balancer. The DNS Server acts as a load-balancer that routes clients to various nodes in the database universe. YBA integrates with [Amazon Route53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html) to provide managed Canonical Name (CNAME) entries for your YugabyteDB universes, and automatically updates the DNS entry as nodes get created, removed, or undergo maintenance. diff --git a/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/azure.md b/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/azure.md index 0e8b120ed1c9..4284d16e7781 100644 --- a/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/azure.md +++ b/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/azure.md @@ -49,13 +49,13 @@ When deploying a universe, YBA uses the provider configuration settings to do th ## Prerequisites -You need to add the following Azure cloud provider credentials via YBA: +You need to add the following Azure cloud provider credentials: +- Application client ID and (if using credentials) client secret +- Resource group name - Subscription ID - Tenant ID - SSH port and user -- Application client ID and secret -- Resource group YBA uses the credentials to automatically provision and deprovision YugabyteDB instances. @@ -105,11 +105,29 @@ Enter a Provider name. The Provider name is an internal tag used for organizing ### Cloud Info -- **Client ID** represents the [ID of an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret) registered in your Azure Active Directory. -- **Client Secret** represents the secret of an application registered in your Azure Active Directory. You need to enter the `Value` of the secret (not the `Secret ID`). -- **Resource Group** represents the group in which YugabyteDB nodes compute and network resources are created. Your Azure Active Directory application (client ID and client secret) needs to have `Network Contributor` and `Virtual Machine Contributor` roles assigned for this resource group. -- **Subscription ID** is required for cost management. The virtual machine resources managed by YBA are tagged with this subscription. -- **Tenant ID** represents the Azure Active Directory tenant ID which belongs to an active subscription. To find your tenant ID, follow instructions provided in [How to find your Azure Active Directory tenant ID](https://learn.microsoft.com/en-us/azure/active-directory/fundamentals/how-to-find-tenant). +Enter the following details of your Azure cloud account, as described in [Azure cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes-azure/). + +#### Client ID + +Provide the ID of the application you registered with the Microsoft Identity Platform. + +#### Credential type + +If you [added credentials](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=client-secret#add-credentials) in the form of a client secret to your registered application: + +1. Select **Specify Client Secret**. +1. Enter the Client Secret of the application associated with the Client ID you provided. You need to enter the `Value` of the secret (not the `Secret ID`). + +If you are using the [managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm) of the Azure VM hosting YugabyteDB Anywhere to authenticate: + +- Select **Use Managed Identity from this YBA host's instance**. + +#### Additional fields + +- **Resource Group** is the name of the resource group you created for your application, and in which YugabyteDB node compute and network resources will be created. +- **Subscription ID** is required for cost management. The virtual machine resources managed by YBA are tagged with this subscription. To get the subscription ID, open Subscriptions in Azure portal and find your subscription. Then, copy the Subscription ID. +- Optionally, if you created a different resource group for your network interfaces, provide the **Network Resource Group** name and the associated **Network Subscription ID**. If you do not provide a Network Resource Group or Subscription ID, network resources will be created in the default resource group. +- **Tenant ID** represents the tenant ID which belongs to an active subscription. To find your tenant ID, follow instructions provided in [How to find your Microsoft Entra tenant ID](https://learn.microsoft.com/en-us/entra/fundamentals/how-to-find-tenant). - **Private DNS zone** lets you use a custom domain name for the nodes in your universe. For details and instructions, see [Define a private DNS zone](#define-a-private-dns-zone). ### Regions diff --git a/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/gcp.md b/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/gcp.md index d282e002d077..8fae19a40c8d 100644 --- a/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/gcp.md +++ b/docs/content/stable/yugabyte-platform/configure-yugabyte-platform/gcp.md @@ -108,7 +108,7 @@ Enter a Provider name. The Provider name is an internal tag used for organizing ### Cloud Info -If your YBA instance is not running inside GCP, you need to supply YBA with credentials to the desired GCP project by uploading a configuration file. To do this, set **Credential Type** to **Upload Service Account config** and proceed to upload the JSON file that you obtained when you created your service account, as described in [Cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes/). +If your YBA instance is not running inside GCP, you need to supply YBA with credentials to the desired GCP project by uploading a configuration file. To do this, set **Credential Type** to **Upload Service Account config** and proceed to upload the JSON file that you obtained when you created your service account, as described in [Cloud permissions](../../prepare/cloud-permissions/cloud-permissions-nodes-gcp/). If your YBA instance is running inside GCP, the preferred method for authentication to the GCP APIs is to add a service account role to the GCP instance running YBA and then configure YBA to use the instance's service account. To do this, set **Credential Type** to **Use service account from this YBA host's instance**. diff --git a/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-aws.md b/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-aws.md index 3525642e82ac..c66935dc4710 100644 --- a/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-aws.md +++ b/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-aws.md @@ -123,8 +123,8 @@ If using a service account, record the following two pieces of information about | Save for later | To configure | | :--- | :--- | -| Access key ID | [AWS cloud provider](../../../configure-yugabyte-platform/aws/) | -| Secret Access Key | [AWS cloud provider](../../../configure-yugabyte-platform/aws/) | +| Access key ID | [AWS provider configuration](../../../configure-yugabyte-platform/aws/) | +| Secret Access Key | | ### IAM role @@ -178,4 +178,4 @@ If you will be using your own custom SSH keys, then ensure that you have them wh | Save for later | To configure | | :--- | :--- | -| Custom SSH keys | [AWS provider](../../../configure-yugabyte-platform/kubernetes/) | +| Custom SSH keys | [AWS provider configuration](../../../configure-yugabyte-platform/kubernetes/) | diff --git a/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-azure.md b/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-azure.md index dfc94e1b63bc..87b68c7f7b70 100644 --- a/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-azure.md +++ b/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-azure.md @@ -52,30 +52,46 @@ The more permissions that you can provide, the more YBA can automate. ## Azure -The following permissions are required for the Azure resource group where you will deploy. +### Application and resource group + +YugabyteDB Anywhere requires cloud permissions to create VMs. You grant YugabyteDB Anywhere access to manage Azure resources such as VMs by registering an application in the Azure portal so the Microsoft identity platform can provide authentication and authorization services for your application. Registering your application establishes a trust relationship between your application and the Microsoft identity platform. + +In addition, your Azure application needs to have a [resource group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/overview#resource-groups) with the following permissions: ```sh Network Contributor -Virtual Machine Contributor +Virtual Machine Contributor ``` -To grant the required access, you can do one of the following: +You can optionally create a resource group for network resources if you want network interfaces to be created separately. The network resource group must have the `Network Contributor` permission. + +For more information on registering applications, refer to [Register an application with the Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=certificate) in the Microsoft Entra documentation. + +For more information on roles, refer to [Assign Azure roles using the Azure portal](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=delegate-condition) in the Microsoft Azure documentation. + +### Credentials + +YugabyteDB Anywhere can authenticate with Azure using one of the following methods: + +- [Add credentials](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=client-secret#add-credentials), in the form of a client secret, to your registered application. -- [Register an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) in the Azure portal so the Microsoft identity platform can provide authentication and authorization services for your application. Registering your application establishes a trust relationship between your application and the Microsoft identity platform. + For information on creating client secrets, see [Create a new client secret](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#option-3-create-a-new-client-secret) in the Microsoft Entra documentation. -- [Assign a managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm) to the Azure VM hosting YugabyteDB Anywhere. +- [Assign a managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm) to the Azure VM hosting YugabyteDB Anywhere. Azure will use the managed identity assigned to your instance to authenticate. -For information on assigning roles to applications, see [Assign a role to an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#assign-a-role-to-the-application); and assigning roles for managed identities, see [Assign Azure roles using the Azure portal](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=delegate-condition) in the Microsoft Azure documentation. + For information on assigning roles for managed identities, see [Assign Azure roles using the Azure portal](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=delegate-condition) in the Microsoft Azure documentation. -If you are registering an application, record the following information about your service account. You will need to provide this information later to YBA. +Record the following information about your service account. You will need to provide this information later when creating an Azure provider configuration. | Save for later | To configure | | :--- | :--- | -| **Service account details** | [Azure cloud provider](../../../configure-yugabyte-platform/azure/) | +| **Service account details** | [Azure provider configuration](../../../configure-yugabyte-platform/azure/) | | Client ID: | | -| Client Secret: | | +| Client Secret:
(not required when using managed identity) | | | Resource Group: | | | Subscription ID: | | +| (Optional) Network Resource Group: | | +| (Optional) Network Subscription ID: | | | Tenant ID: | | ## Managing SSH keys for VMs @@ -89,4 +105,4 @@ If you will be using your own custom SSH keys, then ensure that you have them wh | Save for later | To configure | | :--- | :--- | -| Custom SSH keys | [Azure provider](../../../configure-yugabyte-platform/azure/) | +| Custom SSH keys | [Azure provider configuration](../../../configure-yugabyte-platform/azure/) | diff --git a/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-gcp.md b/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-gcp.md index 87f5929a0ec0..3ce2bbefaaec 100644 --- a/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-gcp.md +++ b/docs/content/stable/yugabyte-platform/prepare/cloud-permissions/cloud-permissions-nodes-gcp.md @@ -70,7 +70,7 @@ Then use one of the following methods: | Save for later | To configure | | :--- | :--- | -| Service account JSON | [GCP cloud provider](../../../configure-yugabyte-platform/gcp/) | +| Service account JSON | [GCP provider configuration](../../../configure-yugabyte-platform/gcp/) | ## Managing SSH keys for VMs @@ -83,4 +83,4 @@ If you will be using your own custom SSH keys, then ensure that you have them wh | Save for later | To configure | | :--- | :--- | -| Custom SSH keys | [GCP provider](../../../configure-yugabyte-platform/gcp/) | +| Custom SSH keys | [GCP provider configuration](../../../configure-yugabyte-platform/gcp/) | diff --git a/docs/content/v2.20/reference/configuration/yb-tserver.md b/docs/content/v2.20/reference/configuration/yb-tserver.md index ffd6cdf57e58..75073fcda92e 100644 --- a/docs/content/v2.20/reference/configuration/yb-tserver.md +++ b/docs/content/v2.20/reference/configuration/yb-tserver.md @@ -732,6 +732,14 @@ Default: `-1` (disables logging statement durations) Specifies the lowest YSQL message level to log. +##### --ysql_output_buffer_size + +Size of YSQL layer output buffer, in bytes. YSQL buffers query responses in this output buffer until either a buffer flush is requested by the client or the buffer overflows. + +As long as no data has been flushed from the buffer, the database can retry queries on retryable errors. For example, you can increase the size of the buffer so that YSQL can retry [read restart errors](../../../architecture/transactions/read-restart-error). + +Default: `262144` (256kB, type: int32) + ### YCQL The following flags support the use of the [YCQL API](../../../api/ycql/): diff --git a/docs/data/currentVersions.json b/docs/data/currentVersions.json index e2dafb06bb82..b9ac51386077 100644 --- a/docs/data/currentVersions.json +++ b/docs/data/currentVersions.json @@ -40,9 +40,9 @@ { "series": "v2.18", "display": "v2.18 (STS)", - "version": "2.18.8.0", + "version": "2.18.8.1", "versionShort": "2.18.8", - "appVersion": "2.18.8.0-b42", + "appVersion": "2.18.8.1-b3", "isStable": true, "isLTS": false, "isSTS": true, diff --git a/docs/netlify.toml b/docs/netlify.toml index 50c2bf78fada..34c83c4c29c4 100644 --- a/docs/netlify.toml +++ b/docs/netlify.toml @@ -747,7 +747,7 @@ [[redirects]] from = "/:version/reference/connectors/*" - to = "/preview/integrations/apache-kafka/" + to = "/preview/explore/change-data-capture/" # Redirect for troubleshoot diff --git a/docs/static/images/architecture/cdc-logical-replication-architecture.png b/docs/static/images/architecture/cdc-logical-replication-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..558d7967d17c77f1f8477a9a262d2914467b1986 GIT binary patch literal 163836 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.#so!(?iFF>0qL=y*+bf!|l9PIXZD7r}+p$^`LR( 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

h3fo@T6YH=8YH66!Yt#F#PFX#oKchFiQvQ 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 literal 0 HcmV?d00001 diff --git a/docs/static/images/architecture/logical_replication_architecture.png b/docs/static/images/architecture/logical_replication_architecture.png deleted file mode 100644 index fff604a94aeeb798160c027052bb10037313dee8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121939 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 dCor @@ -32,3 +31,5 @@ yba: cores: cores memory_size: size volume_size: size + mount_points: + - /mnt/d1 diff --git a/managed/node-agent/resources/ynp/commands/provision_command.py b/managed/node-agent/resources/ynp/commands/provision_command.py index 3c8ae873e4a8..8cd23192950f 100644 --- a/managed/node-agent/resources/ynp/commands/provision_command.py +++ b/managed/node-agent/resources/ynp/commands/provision_command.py @@ -123,7 +123,6 @@ def _generate_template(self): context = self.config[key] context["templatedir"] = os.path.join(os.path.dirname(module[1]), "templates") - logger.info(context) module_instance = module[0]() rendered_template = module_instance.render_templates(context) if rendered_template is not None: diff --git a/managed/node-agent/resources/ynp/configs/config.j2 b/managed/node-agent/resources/ynp/configs/config.j2 index b93148165671..b70ec256f02e 100644 --- a/managed/node-agent/resources/ynp/configs/config.j2 +++ b/managed/node-agent/resources/ynp/configs/config.j2 @@ -32,7 +32,7 @@ nproc_limit = 12000 vm_swappiness = 0 kernel_core_pattern = {{ ynp.yb_home_dir }}/cores/core_%%p_%%t_%%E vm_max_map_count = 262144 -mount_points = {{ ynp.mount_points }} +mount_points = {{ yba.instance_type.mount_points | join(' ') }} [ConfigureOs.limits] core = unlimited @@ -71,5 +71,4 @@ ports = 7000 7100 9000 9100 18018 22 5433 9042 9070 9300 12000 13000 yb_user = yugabyte yb_home_dir = {{ ynp.yb_home_dir }} tmp_directory = {{ ynp.tmp_directory }} -mount_points = {{ ynp.mount_points }} diff --git a/managed/node-agent/resources/ynp/modules/provision/configure_os/templates/precheck.j2 b/managed/node-agent/resources/ynp/modules/provision/configure_os/templates/precheck.j2 index 128e5d9c618e..9268a5888e91 100644 --- a/managed/node-agent/resources/ynp/modules/provision/configure_os/templates/precheck.j2 +++ b/managed/node-agent/resources/ynp/modules/provision/configure_os/templates/precheck.j2 @@ -87,11 +87,13 @@ else add_result "kernel.core_pattern" "FAIL" "kernel.core_pattern is set to $kernel_core_pattern_value (expected: {{ kernel_core_pattern }})" fi -mount_point_array={{ mount_points }} +threshold=49 #Gigabytes +# Convert the space-separated string to an array in bash +IFS=' ' read -r -a mount_points_array <<< {{ mount_points }} # Verify each mount point for mount_point in "${mount_point_array[@]}"; do if [ -d "$mount_point" ]; then - if [ -w "$mount_point" ] && [ "$(stat -c %A "$mount_point" | cut -c 10)" == "w" ]; then + if [ -w "$mount_point" ] && [ $(( $(stat -c %a "$mount_point") % 10 & 2 )) -ne 0 ]; then result="PASS" message="Directory $mount_point exists and is world-writable." echo "[PASS] $message" @@ -107,6 +109,19 @@ for mount_point in "${mount_point_array[@]}"; do echo "[FAIL] $message" any_fail=1 fi - add_result "$mount_point Check" "$result" "$message" + + # Get the available disk space in gigabytes. + free_space_gb=$(df -BG --output=avail "$MOUNT_POINT" | tail -n 1 | tr -d 'G ') + if [ "$free_space_gb" -gt "$threshold" ]; then + result="PASS" + message="Sufficient disk space available: ${AVAILABLE}G" + echo "[PASS] $message" + else + result="FAIL" + message="Insufficient disk space: ${free_space_gb}G available, ${threshold}G required" + echo "[FAIL] $message" + any_fail=1 + fi + add_result "$mount_point Free space check" "$result" "$message" done diff --git a/managed/node-agent/resources/ynp/modules/provision/configure_os/templates/run.j2 b/managed/node-agent/resources/ynp/modules/provision/configure_os/templates/run.j2 index 50e94dbeb573..7cb60c58272c 100644 --- a/managed/node-agent/resources/ynp/modules/provision/configure_os/templates/run.j2 +++ b/managed/node-agent/resources/ynp/modules/provision/configure_os/templates/run.j2 @@ -68,66 +68,4 @@ sysctl -w vm.max_map_count={{ vm_max_map_count }} echo "Kernel settings configured." - -echo "Mounting volumes." -# Excluding the known mounts that we have to avoid. -{% set excluded_mounts = ["/", "boot", "efi"] %} - -# Generate a unique backup filename for /etc/fstab -backup_file="/etc/fstab.bak.$(date +%s)" -cp /etc/fstab $backup_file -echo "Backup of /etc/fstab created at $backup_file." - -# Get list of available volumes excluding the root and specified excluded mounts -volumes=$(lsblk -lnpo NAME,MOUNTPOINT | awk '$2 == "" {print $1}' | grep -vE 'NAME|MOUNTPOINT') - -# Prepare and mount each volume -index=1 -for volume in $volumes; do - if [[ $volume =~ /dev/nvme[0-9]n[0-9]p[0-9]+ ]]; then - echo "Skipping volume $volume" - continue - fi - mount_point="/mnt/d${index}" - - mkdir -p ${mount_point} - - if mount | grep -qE "^${volume}|^${volume}[0-9]"; then - echo "${volume} or one of its partitions is currently mounted. Skipping..." - continue - fi - - # Check if filesystem already exists - if ! blkid ${volume} | grep -q "TYPE=\"xfs\""; then - mkfs -t xfs ${volume} - echo "Filesystem created on ${volume}." - else - echo "Filesystem already exists on ${volume}." - fi - - # Add entry to /etc/fstab if it doesn't already exist - if ! grep -q "^${volume}" /etc/fstab; then - echo "${volume} ${mount_point} xfs noatime 0 0" | tee -a /etc/fstab - else - echo "Entry for ${volume} already exists in /etc/fstab." - fi - - # Mount the volume if it's not already mounted - if ! mountpoint -q ${mount_point}; then - mount ${mount_point} || fail "Failed to mount ${volume} at ${mount_point}." - echo "Mounted ${volume} at ${mount_point}." - else - echo "${mount_point} is already mounted." - fi - - # Set ownership and permissions - chown yugabyte:yugabyte ${mount_point} || fail "Failed to set ownership for ${mount_point}." - chmod 755 ${mount_point} || fail "Failed to set permissions for ${mount_point}." - echo "Set ownership and permissions for ${mount_point}." - - index=$((index + 1)) -done - -echo "Volume mounting process completed." - echo "OS Configuration applied successfully." diff --git a/managed/node-agent/resources/ynp/modules/provision/node_agent/node_agent.py b/managed/node-agent/resources/ynp/modules/provision/node_agent/node_agent.py index 0ef27cd2b52c..4d78a807397c 100644 --- a/managed/node-agent/resources/ynp/modules/provision/node_agent/node_agent.py +++ b/managed/node-agent/resources/ynp/modules/provision/node_agent/node_agent.py @@ -104,7 +104,7 @@ def _generate_instance_type_payload(self, context): 'volumeDetailsList': [] } } - mount_points = context.get('instance_type_mount_points').split(',') + mount_points = context.get('instance_type_mount_points').strip("[]").replace("'", "").split(", ") for mp in mount_points: volume_detail = { 'volumeSizeGB': context.get('instance_type_volume_size'), @@ -167,7 +167,7 @@ def _create_instance_if_not_exists(self, context, provider): logging.error(f"Request error: {req_err}") except ValueError as json_err: logging.error(f"Error parsing JSON response: {json_err}") - + def render_templates(self, context): node_agent_enabled = False yba_url = context.get('url') @@ -219,7 +219,7 @@ def render_templates(self, context): except requests.exceptions.RequestException as req_err: logging.error(f"Request error: {req_err}") - + add_node_payload = self._generate_add_node_payload(context) add_node_payload_file = os.path.join(context.get('tmp_directory'), 'add_node_to_provider.json') with open(add_node_payload_file, 'w') as f: diff --git a/managed/node-agent/util/certs_util.go b/managed/node-agent/util/certs_util.go index e5ebcc75984c..ce89dd7b15e4 100644 --- a/managed/node-agent/util/certs_util.go +++ b/managed/node-agent/util/certs_util.go @@ -156,7 +156,7 @@ func ServerKeyPath(config *Config) string { // The JWT is signed using the key in the certs directory. func GenerateJWT(ctx context.Context, config *Config) (string, error) { keyFilepath := ServerKeyPath(config) - privateKey, err := ioutil.ReadFile(keyFilepath) + privateKey, err := os.ReadFile(keyFilepath) if err != nil { FileLogger().Errorf(ctx, "Error while reading the private key: %s", err.Error()) return "", err diff --git a/managed/src/main/java/com/yugabyte/yw/models/TaskInfo.java b/managed/src/main/java/com/yugabyte/yw/models/TaskInfo.java index f1fa83c35c8b..55c58ab495cf 100644 --- a/managed/src/main/java/com/yugabyte/yw/models/TaskInfo.java +++ b/managed/src/main/java/com/yugabyte/yw/models/TaskInfo.java @@ -398,8 +398,8 @@ public static List findDuplicateDeleteBackupTasks(UUID customerUUID, U .eq("task_type", TaskType.DeleteBackup) .ne("task_state", State.Failure) .ne("task_state", State.Aborted) - .eq("details->>'customerUUID'", customerUUID.toString()) - .eq("details->>'backupUUID'", backupUUID.toString()) + .eq("task_params->>'customerUUID'", customerUUID.toString()) + .eq("task_params->>'backupUUID'", backupUUID.toString()) .findList(); } @@ -409,8 +409,8 @@ public static List findIncompleteDeleteBackupTasks(UUID customerUUID, .where() .in("task_type", TaskType.DeleteBackup, TaskType.DeleteBackupYb) .in("task_state", INCOMPLETE_STATES) - .eq("details->>'customerUUID'", customerUUID.toString()) - .eq("details->>'backupUUID'", backupUUID.toString()) + .eq("task_params->>'customerUUID'", customerUUID.toString()) + .eq("task_params->>'backupUUID'", backupUUID.toString()) .findList(); } } diff --git a/managed/src/main/java/com/yugabyte/yw/models/configs/validators/GCPProviderValidator.java b/managed/src/main/java/com/yugabyte/yw/models/configs/validators/GCPProviderValidator.java index 033ebbe1fe60..dd4da17b8652 100644 --- a/managed/src/main/java/com/yugabyte/yw/models/configs/validators/GCPProviderValidator.java +++ b/managed/src/main/java/com/yugabyte/yw/models/configs/validators/GCPProviderValidator.java @@ -1,5 +1,7 @@ package com.yugabyte.yw.models.configs.validators; +import static play.mvc.Http.Status.BAD_REQUEST; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.google.api.services.compute.model.Firewall; @@ -39,6 +41,7 @@ public class GCPProviderValidator extends ProviderFieldsValidator { private final GCPCloudImpl gcpCloudImpl; private final RuntimeConfGetter runtimeConfGetter; + private final String INSTANCE_TEMPLATE_REGEX = "[a-z]([-a-z0-9]*[a-z0-9])?"; @Inject public GCPProviderValidator( @@ -229,6 +232,12 @@ private void validateInstanceTempl( .get("jsonPath") .asText(); try { + if (!instanceTemplate.matches(INSTANCE_TEMPLATE_REGEX)) { + throw new PlatformServiceException( + BAD_REQUEST, + "Instance template must start with a lowercase character and can only contain" + + " alphanumeric characters and '-'"); + } if (!apiClient.checkInstanceTempelate(instanceTemplate)) { String errorMsg = String.format( diff --git a/python/yugabyte/thirdparty_tool.py b/python/yugabyte/thirdparty_tool.py index 1f08b6e814ca..27f641cfceaa 100755 --- a/python/yugabyte/thirdparty_tool.py +++ b/python/yugabyte/thirdparty_tool.py @@ -69,7 +69,9 @@ # These were incorrectly used without the "clang" prefix to indicate various versions of Clang. NUMBER_ONLY_VERSIONS_OF_CLANG = [str(i) for i in [12, 13, 14]] -PREFERRED_OS_TYPE = 'centos7' +# Linux distribution with the oldest glibc available to us. Third-party archives built on this +# OS can be used on newer Linux distributions, unless we need ASAN/TSAN. +PREFERRED_OS_TYPE = 'amzn2' ThirdPartyArchivesYAML = Dict[str, Union[str, List[Dict[str, str]]]] @@ -272,13 +274,14 @@ def is_consistent_with_yb_version(self, yb_version: str) -> bool: def should_skip_as_too_os_specific(self) -> bool: """ - Certain build types of specific OSes could be skipped because we can use the CentOS 7 build - instead. We can do that in cases we know we don't need to run ASAN/TSAN. We know that we - don't use ASAN/TSAN on aarch64 or for LTO builds as of 11/07/2022. Also we don't skip - Linuxbrew builds or GCC builds. + Certain build types of specific OSes could be skipped because we can use our "preferred OS + type", the supported Linux distribution with the oldest glibc version, instead. We can do + that in cases we know we don't need to run ASAN/TSAN. We know that we don't use ASAN/TSAN + on aarch64 or for LTO builds as of 11/07/2022. Also we don't skip Linuxbrew builds or GCC + builds. """ return ( - self.os_type != 'centos7' and + self.os_type != PREFERRED_OS_TYPE and self.compiler_type.startswith('clang') and # We handle Linuxbrew builds in a special way, e.g. they could be built on AlmaLinux 8. not self.is_linuxbrew and diff --git a/src/postgres/src/backend/storage/lmgr/predicate.c b/src/postgres/src/backend/storage/lmgr/predicate.c index 25e7e4e37bfd..5b12a01271f3 100644 --- a/src/postgres/src/backend/storage/lmgr/predicate.c +++ b/src/postgres/src/backend/storage/lmgr/predicate.c @@ -210,6 +210,8 @@ #include "utils/rel.h" #include "utils/snapmgr.h" +#include "pg_yb_utils.h" + /* Uncomment the next line to test the graceful degradation code. */ /* #define TEST_SUMMARIZE_SERIAL */ @@ -1699,8 +1701,28 @@ GetSerializableTransactionSnapshot(Snapshot snapshot) * A special optimization is available for SERIALIZABLE READ ONLY * DEFERRABLE transactions -- we can wait for a suitable snapshot and * thereby avoid all SSI overhead once it's running. + * + * YB: PG uses SSI to implement serializable isolation level. + * YB uses 2PL (i.e., two phase locking) for the same. + * Both YB and PG expose a different functionality using + * the same DEFERRABLE keyword. + * + * In PG, READ ONLY DEFERRABLE allows a read only transaction + * to avoid serialization errors with concurrent serializable + * transactions, by waiting for the concurrent transactions to + * complete first. + * + * In YB, there is no cycle detection algorithm. Instead READ ONLY + * serializable transactions are essentially equivalent to a + * READ ONLY snapshot isolation transaction (see #23213). + * YB uses DEFERRABLE to allow READ ONLY transactions to wait out + * the maximum possible clock skew (i.e., max_clock_skew_usec) + * so as to avoid read restart errors. + * + * Given this difference, YB need not wait for a safe snapshot + * i.e., concurrent transactions need not be waited for. */ - if (XactReadOnly && XactDeferrable) + if (!YBTransactionsEnabled() && XactReadOnly && XactDeferrable) return GetSafeSnapshot(snapshot); return GetSerializableTransactionSnapshotInt(snapshot, diff --git a/src/postgres/src/test/isolation/expected/modify-transaction-characteristics.out b/src/postgres/src/test/isolation/expected/modify-transaction-characteristics.out index 97ba5e902c3f..6d1c9e3a72c5 100644 --- a/src/postgres/src/test/isolation/expected/modify-transaction-characteristics.out +++ b/src/postgres/src/test/isolation/expected/modify-transaction-characteristics.out @@ -507,12 +507,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_rc_method2 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -522,12 +521,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_rc_method3_part1 s2_method3_part2 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -538,12 +536,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_rc_method4 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -553,12 +550,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_rr_method1 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -568,12 +564,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_rr_method2 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -583,12 +578,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_rr_method3_part1 s2_method3_part2 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -599,12 +593,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_rr_method4 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -614,12 +607,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_sr_method1 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -629,12 +621,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_sr_method2 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -644,12 +635,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_sr_method3_part1 s2_method3_part2 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -660,12 +650,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s2_begin_sr_method4 s2_switch_to_sr s2_read_only s2_deferrable s1_begin_sr_method1 s1_update s2_select s1_commit s2_commit @@ -675,12 +664,11 @@ step s2_read_only: SET TRANSACTION READ ONLY; step s2_deferrable: SET TRANSACTION DEFERRABLE; step s1_begin_sr_method1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; step s1_update: UPDATE test SET v=v+1 WHERE k=1; -step s2_select: SELECT * FROM test; -step s1_commit: COMMIT; -step s2_select: <... completed> +step s2_select: SELECT * FROM test; k v -1 3 +1 2 +step s1_commit: COMMIT; step s2_commit: COMMIT; starting permutation: s1_begin_rc_method1 s1_switch_to_sr s1_read_only s1_read_write s2_update s1_commit diff --git a/src/yb/docdb/consensus_frontier.cc b/src/yb/docdb/consensus_frontier.cc index 7e1b8a83bb83..28acb5c738de 100644 --- a/src/yb/docdb/consensus_frontier.cc +++ b/src/yb/docdb/consensus_frontier.cc @@ -349,6 +349,20 @@ void ConsensusFrontier::ResetSchemaVersion() { cotable_schema_versions_.clear(); } +bool ConsensusFrontier::UpdateCoTableId(const Uuid& cotable_id, const Uuid& new_cotable_id) { + if (cotable_id == new_cotable_id) { + return false; + } + auto it = cotable_schema_versions_.find(cotable_id); + if (it == cotable_schema_versions_.end()) { + return false; + } + auto schema_version = it->second; + cotable_schema_versions_.erase(it); + cotable_schema_versions_[new_cotable_id] = schema_version; + return true; +} + void ConsensusFrontier::MakeExternalSchemaVersionsAtMost( std::unordered_map* min_schema_versions) const { if (primary_schema_version_) { diff --git a/src/yb/docdb/consensus_frontier.h b/src/yb/docdb/consensus_frontier.h index 963b6f23030c..3eda9a81b29a 100644 --- a/src/yb/docdb/consensus_frontier.h +++ b/src/yb/docdb/consensus_frontier.h @@ -120,6 +120,10 @@ class ConsensusFrontier : public rocksdb::UserFrontier { void AddSchemaVersion(const Uuid& table_id, SchemaVersion version); void ResetSchemaVersion(); + // Update cotable_id to new_cotable_id in current frontier's cotable_schema_versions_ map. + // Return true if the map is modified, otherwise, return false. + bool UpdateCoTableId(const Uuid& cotable_id, const Uuid& new_cotable_id); + // Merge current frontier with provided map, preferring min values. void MakeExternalSchemaVersionsAtMost( std::unordered_map* min_schema_versions) const; diff --git a/src/yb/docdb/docdb_rocksdb_util.cc b/src/yb/docdb/docdb_rocksdb_util.cc index d8fb2c23bbd6..9020a009cdf1 100644 --- a/src/yb/docdb/docdb_rocksdb_util.cc +++ b/src/yb/docdb/docdb_rocksdb_util.cc @@ -932,7 +932,8 @@ class RocksDBPatcher::Impl { return helper.Apply(options_, imm_cf_options_); } - Status ModifyFlushedFrontier(const ConsensusFrontier& frontier) { + Status ModifyFlushedFrontier( + const ConsensusFrontier& frontier, const CotableIdsMap& cotable_ids_map) { RocksDBPatcherHelper helper(&version_set_); docdb::ConsensusFrontier final_frontier = frontier; @@ -952,7 +953,8 @@ class RocksDBPatcher::Impl { helper.Edit().ModifyFlushedFrontier( final_frontier.Clone(), rocksdb::FrontierModificationMode::kForce); - helper.IterateFiles([&helper, &frontier](int level, rocksdb::FileMetaData fmd) { + helper.IterateFiles([&helper, &frontier, &cotable_ids_map]( + int level, rocksdb::FileMetaData fmd) { bool modified = false; for (auto* user_frontier : {&fmd.smallest.user_frontier, &fmd.largest.user_frontier}) { if (!*user_frontier) { @@ -967,6 +969,11 @@ class RocksDBPatcher::Impl { consensus_frontier.set_history_cutoff_information(frontier.history_cutoff()); modified = true; } + for (const auto& [table_id, new_table_id] : cotable_ids_map) { + if (consensus_frontier.UpdateCoTableId(table_id, new_table_id)) { + modified = true; + } + } } if (modified) { helper.ModifyFile(level, fmd); @@ -1043,8 +1050,9 @@ Status RocksDBPatcher::SetHybridTimeFilter(std::optional db_oid, Hybri return impl_->SetHybridTimeFilter(db_oid, value); } -Status RocksDBPatcher::ModifyFlushedFrontier(const ConsensusFrontier& frontier) { - return impl_->ModifyFlushedFrontier(frontier); +Status RocksDBPatcher::ModifyFlushedFrontier( + const ConsensusFrontier& frontier, const CotableIdsMap& cotable_ids_map) { + return impl_->ModifyFlushedFrontier(frontier, cotable_ids_map); } Status RocksDBPatcher::UpdateFileSizes() { diff --git a/src/yb/docdb/docdb_rocksdb_util.h b/src/yb/docdb/docdb_rocksdb_util.h index 095155b2a8e8..bdc032ce3da8 100644 --- a/src/yb/docdb/docdb_rocksdb_util.h +++ b/src/yb/docdb/docdb_rocksdb_util.h @@ -32,6 +32,10 @@ namespace yb { namespace docdb { +// Map from old cotable id to new cotable id. +// Used to restore snapshot to a new database/tablegroup and update cotable ids in the frontiers. +using CotableIdsMap = std::unordered_map; + const int kDefaultGroupNo = 0; dockv::KeyBytes AppendDocHt(Slice key, const DocHybridTime& doc_ht); @@ -135,7 +139,8 @@ class RocksDBPatcher { Status SetHybridTimeFilter(std::optional db_oid, HybridTime value); // Modify flushed frontier and clean up smallest/largest op id in per-SST file metadata. - Status ModifyFlushedFrontier(const ConsensusFrontier& frontier); + Status ModifyFlushedFrontier( + const ConsensusFrontier& frontier, const CotableIdsMap& cotable_ids_map); // Update file sizes in manifest if actual file size was changed because of direct manipulation // with .sst files. diff --git a/src/yb/integration-tests/xcluster/xcluster-test.cc b/src/yb/integration-tests/xcluster/xcluster-test.cc index bc96e352b8aa..a873c0bdfd8d 100644 --- a/src/yb/integration-tests/xcluster/xcluster-test.cc +++ b/src/yb/integration-tests/xcluster/xcluster-test.cc @@ -124,8 +124,6 @@ DECLARE_int32(log_min_seconds_to_retain); DECLARE_int32(log_min_segments_to_retain); DECLARE_uint64(log_segment_size_bytes); DECLARE_int64(log_stop_retaining_min_disk_mb); -DECLARE_int32(ns_replication_sync_backoff_secs); -DECLARE_int32(ns_replication_sync_retry_secs); DECLARE_uint32(replication_failure_delay_exponent); DECLARE_int64(rpc_throttle_threshold_bytes); DECLARE_int32(transaction_table_num_tablets); diff --git a/src/yb/integration-tests/xcluster/xcluster_test_base.cc b/src/yb/integration-tests/xcluster/xcluster_test_base.cc index ff24bd334894..239ae1934e04 100644 --- a/src/yb/integration-tests/xcluster/xcluster_test_base.cc +++ b/src/yb/integration-tests/xcluster/xcluster_test_base.cc @@ -1010,7 +1010,7 @@ Result XClusterTestBase::GetProducerMasterProxy( Status XClusterTestBase::ClearFailedUniverse(Cluster& cluster) { auto& catalog_manager = VERIFY_RESULT(cluster.mini_cluster_->GetLeaderMiniMaster())->catalog_manager_impl(); - return catalog_manager.ClearFailedUniverse(); + return catalog_manager.ClearFailedUniverse(catalog_manager.GetLeaderEpochInternal()); } } // namespace yb diff --git a/src/yb/integration-tests/xcluster/xcluster_ysql_colocated-test.cc b/src/yb/integration-tests/xcluster/xcluster_ysql_colocated-test.cc index 37fa87033231..a29acb61ff41 100644 --- a/src/yb/integration-tests/xcluster/xcluster_ysql_colocated-test.cc +++ b/src/yb/integration-tests/xcluster/xcluster_ysql_colocated-test.cc @@ -1,4 +1,4 @@ -// Copyright (c) YugabyteDB, Inc. +// Copyright (c) YugaByte, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except // in compliance with the License. You may obtain a copy of the License at @@ -23,6 +23,7 @@ #include "yb/master/master_ddl.proxy.h" #include "yb/master/master_replication.proxy.h" #include "yb/master/mini_master.h" +#include "yb/tserver/mini_tablet_server.h" #include "yb/util/backoff_waiter.h" DECLARE_bool(xcluster_wait_on_ddl_alter); @@ -118,6 +119,7 @@ class XClusterYsqlColocatedTest : public XClusterYsqlTestBase { auto& tables = onlyColocated ? colocated_consumer_tables : consumer_tables_; for (const auto& consumer_table : tables) { LOG(INFO) << "Checking records for table " << consumer_table->name().ToString(); + RETURN_NOT_OK(WaitForRowCount(consumer_table->name(), num_results, &consumer_cluster_)); RETURN_NOT_OK(ValidateRows(consumer_table->name(), num_results, &consumer_cluster_)); } return true; @@ -196,20 +198,50 @@ class XClusterYsqlColocatedTest : public XClusterYsqlTestBase { [&]() -> Result { LOG(INFO) << "Checking records for table " << new_colocated_consumer_table->name().ToString(); - RETURN_NOT_OK( - ValidateRows(new_colocated_consumer_table->name(), kRecordBatch, &consumer_cluster_)); + RETURN_NOT_OK(WaitForRowCount( + new_colocated_consumer_table->name(), kRecordBatch, &consumer_cluster_)); + RETURN_NOT_OK(ValidateRows( + new_colocated_consumer_table->name(), kRecordBatch, &consumer_cluster_)); return true; }, MonoDelta::FromSeconds(20 * kTimeMultiplier), "IsDataReplicatedCorrectly new colocated table")); - // 6. Drop the new table and ensure that data is getting replicated correctly for + // 6. Shutdown the colocated tablet leader and verify that replication is still happening. + { + auto tablet_ids = ListTabletIdsForTable(consumer_cluster(), colocated_parent_table_id); + auto old_ts = FindTabletLeader(consumer_cluster(), *tablet_ids.begin()); + old_ts->Shutdown(); + const auto deadline = CoarseMonoClock::Now() + 10s * kTimeMultiplier; + RETURN_NOT_OK(WaitUntilTabletHasLeader(consumer_cluster(), *tablet_ids.begin(), deadline)); + RETURN_NOT_OK(old_ts->RestartStoppedServer()); + RETURN_NOT_OK(old_ts->WaitStarted()); + + RETURN_NOT_OK(InsertRowsInProducer( + kRecordBatch, 2 * kRecordBatch, new_colocated_producer_table, + use_transaction)); + + RETURN_NOT_OK(WaitFor( + [&]() -> Result { + LOG(INFO) << "Checking records for table " + << new_colocated_consumer_table->name().ToString(); + RETURN_NOT_OK(WaitForRowCount( + new_colocated_consumer_table->name(), 2 * kRecordBatch, &consumer_cluster_)); + RETURN_NOT_OK(ValidateRows( + new_colocated_consumer_table->name(), 2 * kRecordBatch, &consumer_cluster_)); + return true; + }, + MonoDelta::FromSeconds(20 * kTimeMultiplier), + "IsDataReplicatedCorrectly new colocated table")); + } + + // 7. Drop the new table and ensure that data is getting replicated correctly for // the other tables RETURN_NOT_OK( DropYsqlTable(&producer_cluster_, namespace_name, "", Format("test_table_$0", idx))); LOG(INFO) << Format("Dropped test_table_$0 on Producer side", idx); - // 7. Add additional data to the original tables. + // 8. Add additional data to the original tables. for (const auto& producer_table : producer_tables_) { LOG(INFO) << "Writing records for table " << producer_table->name().ToString(); RETURN_NOT_OK( @@ -217,7 +249,7 @@ class XClusterYsqlColocatedTest : public XClusterYsqlTestBase { } count += kRecordBatch; - // 8. Verify all tables are properly replicated. + // 9. Verify all tables are properly replicated. RETURN_NOT_OK(WaitFor( [&]() -> Result { return data_replicated_correctly(count, false); }, MonoDelta::FromSeconds(20 * kTimeMultiplier), diff --git a/src/yb/master/CMakeLists.txt b/src/yb/master/CMakeLists.txt index b9faacdf2603..6f78ade89ea5 100644 --- a/src/yb/master/CMakeLists.txt +++ b/src/yb/master/CMakeLists.txt @@ -138,6 +138,7 @@ set(MASTER_SRCS xcluster/add_table_to_xcluster_source_task.cc xcluster/add_table_to_xcluster_target_task.cc xcluster/master_xcluster_util.cc + xcluster/xcluster_bootstrap_helper.cc xcluster/xcluster_catalog_entity.cc xcluster/xcluster_config.cc xcluster/xcluster_consumer_metrics.cc @@ -148,6 +149,8 @@ set(MASTER_SRCS xcluster/xcluster_safe_time_service.cc xcluster/xcluster_source_manager.cc xcluster/xcluster_target_manager.cc + xcluster/xcluster_universe_replication_alter_helper.cc + xcluster/xcluster_universe_replication_setup_helper.cc xrepl_catalog_manager.cc yql_aggregates_vtable.cc yql_auth_resource_role_permissions_index.cc diff --git a/src/yb/master/catalog_manager.cc b/src/yb/master/catalog_manager.cc index e0dfe59c79b7..f8a5cc343564 100644 --- a/src/yb/master/catalog_manager.cc +++ b/src/yb/master/catalog_manager.cc @@ -12896,5 +12896,22 @@ CatalogManager::GetStatefulServicesStatus() const { return result; } +Status CatalogManager::GetTableGroupAndColocationInfo( + const TableId& table_id, TablegroupId& out_tablegroup_id, bool& out_colocated_database) { + SharedLock lock(mutex_); + const auto* tablegroup = tablegroup_manager_->FindByTable(table_id); + SCHECK_FORMAT(tablegroup, NotFound, "No tablegroup found for table: $0", table_id); + + out_tablegroup_id = tablegroup->id(); + + auto ns = FindPtrOrNull(namespace_ids_map_, tablegroup->database_id()); + SCHECK( + ns, NotFound, + Format("Could not find namespace by namespace id $0", tablegroup->database_id())); + out_colocated_database = ns->colocated(); + + return Status::OK(); +} + } // namespace master } // namespace yb diff --git a/src/yb/master/catalog_manager.h b/src/yb/master/catalog_manager.h index fcd7a685b845..dce20e507953 100644 --- a/src/yb/master/catalog_manager.h +++ b/src/yb/master/catalog_manager.h @@ -160,8 +160,6 @@ typedef std::unordered_map> TableToTables typedef std::unordered_map> TableToTabletInfos; -typedef std::unordered_map TableBootstrapIdsMap; - constexpr int32_t kInvalidClusterConfigVersion = 0; YB_DEFINE_ENUM( @@ -1276,11 +1274,6 @@ class CatalogManager : public tserver::TabletPeerLookupIf, rpc::RpcContext* rpc, const LeaderEpoch& epoch); - Status InitXClusterConsumer( - const std::vector& consumer_info, const std::string& master_addrs, - UniverseReplicationInfo& replication_info, - std::shared_ptr xcluster_rpc_tasks); - void HandleCreateTabletSnapshotResponse(TabletInfo* tablet, bool error) override; void HandleRestoreTabletSnapshotResponse(TabletInfo* tablet, bool error) override; @@ -1372,44 +1365,6 @@ class CatalogManager : public tserver::TabletPeerLookupIf, const GetUDTypeMetadataRequestPB* req, GetUDTypeMetadataResponsePB* resp, rpc::RpcContext* rpc); - // Bootstrap namespace and setup replication to consume data from another YB universe. - Status SetupNamespaceReplicationWithBootstrap( - const SetupNamespaceReplicationWithBootstrapRequestPB* req, - SetupNamespaceReplicationWithBootstrapResponsePB* resp, rpc::RpcContext* rpc, - const LeaderEpoch& epoch); - - // Setup Universe Replication to consume data from another YB universe. - Status SetupUniverseReplication( - const SetupUniverseReplicationRequestPB* req, - SetupUniverseReplicationResponsePB* resp, - rpc::RpcContext* rpc); - - // Delete Universe Replication. - Status DeleteUniverseReplication( - const DeleteUniverseReplicationRequestPB* req, - DeleteUniverseReplicationResponsePB* resp, - rpc::RpcContext* rpc); - - // Alter Universe Replication. - Status AlterUniverseReplication( - const AlterUniverseReplicationRequestPB* req, AlterUniverseReplicationResponsePB* resp, - rpc::RpcContext* rpc, const LeaderEpoch& epoch); - - Status UpdateProducerAddress( - scoped_refptr universe, - const AlterUniverseReplicationRequestPB* req); - - Status AddTablesToReplication( - scoped_refptr universe, - const AlterUniverseReplicationRequestPB* req, - AlterUniverseReplicationResponsePB* resp, - rpc::RpcContext* rpc); - - // Rename an existing Universe Replication. - Status RenameUniverseReplication( - scoped_refptr universe, - const AlterUniverseReplicationRequestPB* req); - Status ChangeXClusterRole( const ChangeXClusterRoleRequestPB* req, ChangeXClusterRoleResponsePB* resp, @@ -1436,17 +1391,6 @@ class CatalogManager : public tserver::TabletPeerLookupIf, std::vector> GetAllUniverseReplications() const; - // Checks if the universe is in an active state or has failed during setup. - Status IsSetupUniverseReplicationDone( - const IsSetupUniverseReplicationDoneRequestPB* req, - IsSetupUniverseReplicationDoneResponsePB* resp, - rpc::RpcContext* rpc); - - // Checks if the replication bootstrap is done, or return its current state. - Status IsSetupNamespaceReplicationWithBootstrapDone( - const IsSetupNamespaceReplicationWithBootstrapDoneRequestPB* req, - IsSetupNamespaceReplicationWithBootstrapDoneResponsePB* resp, rpc::RpcContext* rpc); - // On a producer side split, creates new pollers on the consumer for the new tablet children. Status UpdateConsumerOnProducerSplit( const UpdateConsumerOnProducerSplitRequestPB* req, @@ -1541,7 +1485,7 @@ class CatalogManager : public tserver::TabletPeerLookupIf, Result GetNumLiveTServersForActiveCluster() override; - Status ClearFailedUniverse(); + Status ClearFailedUniverse(const LeaderEpoch& epoch); void SetCDCServiceEnabled(); @@ -1622,6 +1566,42 @@ class CatalogManager : public tserver::TabletPeerLookupIf, Result> GetTableKeyRanges(const TableId& table_id); + Result GetTableSchemaVersion(const TableId& table_id); + + Status GetTableGroupAndColocationInfo( + const TableId& table_id, TablegroupId& out_tablegroup_id, bool& out_colocated_database) + EXCLUDES(mutex_); + + void InsertNewUniverseReplication(UniverseReplicationInfo& replication_group) EXCLUDES(mutex_); + + void MarkReplicationBootstrapForCleanup(const xcluster::ReplicationGroupId& replication_group_id) + EXCLUDES(mutex_); + + // Will return nullptr if not found. + scoped_refptr GetUniverseReplicationBootstrap( + const xcluster::ReplicationGroupId& replication_group_id) EXCLUDES(mutex_); + + void InsertNewUniverseReplicationInfoBootstrapInfo( + UniverseReplicationBootstrapInfo& bootstrap_info) EXCLUDES(mutex_); + + // All entities must be write locked. + Status ReplaceUniverseReplication( + const UniverseReplicationInfo& old_replication_group, + UniverseReplicationInfo& new_replication_group, const ClusterConfigInfo& cluster_config, + const LeaderEpoch& epoch) EXCLUDES(mutex_); + + void RemoveUniverseReplicationFromMap(const xcluster::ReplicationGroupId& replication_group_id) + EXCLUDES(mutex_); + + Status DoImportSnapshotMeta( + const SnapshotInfoPB& snapshot_pb, const LeaderEpoch& epoch, + const std::optional& clone_target_namespace_name, NamespaceMap* namespace_map, + UDTypeMap* type_map, ExternalTableSnapshotDataMap* tables_data, + CoarseTimePoint deadline) override; + + Status CreateTransactionAwareSnapshot( + const CreateSnapshotRequestPB& req, CreateSnapshotResponsePB* resp, CoarseTimePoint deadline); + protected: // TODO Get rid of these friend classes and introduce formal interface. friend class TableLoader; @@ -2123,37 +2103,6 @@ class CatalogManager : public tserver::TabletPeerLookupIf, Result IsCreateTableDone(const TableInfoPtr& table); - // SetupReplicationWithBootstrap - Status ValidateReplicationBootstrapRequest( - const SetupNamespaceReplicationWithBootstrapRequestPB* req); - - void DoReplicationBootstrap( - const xcluster::ReplicationGroupId& replication_id, - const std::vector& tables, - Result bootstrap_producer_result); - - Result DoReplicationBootstrapCreateSnapshot( - const std::vector& tables, - scoped_refptr bootstrap_info); - - using TableMetaPB = ImportSnapshotMetaResponsePB::TableMetaPB; - Result> DoReplicationBootstrapImportSnapshot( - const SnapshotInfoPB& snapshot, - scoped_refptr bootstrap_info); - - Status DoReplicationBootstrapTransferAndRestoreSnapshot( - const std::vector& tables_meta, - scoped_refptr bootstrap_info); - - void MarkReplicationBootstrapFailed( - scoped_refptr bootstrap_info, const Status& failure_status); - // Sets the appropriate failure state and the error status on the replication bootstrap and - // commits the mutation to the sys catalog. - void MarkReplicationBootstrapFailed( - const Status& failure_status, - CowWriteLock* bootstrap_info_lock, - scoped_refptr bootstrap_info); - struct CleanupFailedReplicationBootstrapInfo { // State that the task failed on. SysUniverseReplicationBootstrapEntryPB::State state; @@ -2560,14 +2509,6 @@ class CatalogManager : public tserver::TabletPeerLookupIf, const SysRowEntry& entry, const SnapshotId& snapshot_id, const LeaderEpoch& epoch) REQUIRES(mutex_); - Status DoImportSnapshotMeta( - const SnapshotInfoPB& snapshot_pb, - const LeaderEpoch& epoch, - const std::optional& clone_target_namespace_name, - NamespaceMap* namespace_map, - UDTypeMap* type_map, - ExternalTableSnapshotDataMap* tables_data, - CoarseTimePoint deadline) override; Status ImportSnapshotPreprocess( const SnapshotInfoPB& snapshot_pb, const LeaderEpoch& epoch, @@ -2640,8 +2581,6 @@ class CatalogManager : public tserver::TabletPeerLookupIf, Status PreprocessTabletEntry(const SysRowEntry& entry, ExternalTableSnapshotDataMap* table_map); Status ImportTabletEntry(const SysRowEntry& entry, ExternalTableSnapshotDataMap* table_map); - Result GetTableSchemaVersion(const TableId& table_id); - Result CollectEntries( const google::protobuf::RepeatedPtrField& tables, CollectFlags flags); @@ -2776,133 +2715,18 @@ class CatalogManager : public tserver::TabletPeerLookupIf, const TSHeartbeatRequestPB* req, TSHeartbeatResponsePB* resp); - // Helper functions for GetTableSchemaCallback, GetTablegroupSchemaCallback - // and GetColocatedTabletSchemaCallback. - - // Helper container to track colocationId and the producer to consumer schema version mapping. - typedef std::vector> - ColocationSchemaVersions; - - struct SetupReplicationInfo { - std::unordered_map table_bootstrap_ids; - bool transactional; - }; - - Status ValidateMasterAddressesBelongToDifferentCluster( - const google::protobuf::RepeatedPtrField& master_addresses); - - // Validates a single table's schema with the corresponding table on the consumer side, and - // updates consumer_table_id with the new table id. Return the consumer table schema if the - // validation is successful. - Status ValidateTableSchemaForXCluster( - const client::YBTableInfo& info, const SetupReplicationInfo& setup_info, - GetTableSchemaResponsePB* resp); - - // Adds a validated table to the sys catalog table map for the given universe - Status AddValidatedTableToUniverseReplication( - scoped_refptr universe, - const TableId& producer_table, - const TableId& consumer_table, - const SchemaVersion& producer_schema_version, - const SchemaVersion& consumer_schema_version, - const ColocationSchemaVersions& colocated_schema_versions); - Status AddSchemaVersionMappingToUniverseReplication( scoped_refptr universe, ColocationId consumer_table, const SchemaVersion& producer_schema_version, const SchemaVersion& consumer_schema_version); - // If all tables have been validated, creates a CDC stream for each table. - Status CreateCdcStreamsIfReplicationValidated( - scoped_refptr universe, - const std::unordered_map& table_bootstrap_ids); - - Status AddValidatedTableAndCreateCdcStreams( - scoped_refptr universe, - const std::unordered_map& table_bootstrap_ids, - const TableId& producer_table, - const TableId& consumer_table, - const ColocationSchemaVersions& colocated_schema_versions); - - Status ValidateTableAndCreateCdcStreams( - scoped_refptr universe, - const std::shared_ptr& producer_info, - const SetupReplicationInfo& setup_info); - - void GetTableSchemaCallback( - const xcluster::ReplicationGroupId& replication_group_id, - const std::shared_ptr& producer_info, - const SetupReplicationInfo& setup_info, const Status& s); - void GetTablegroupSchemaCallback( - const xcluster::ReplicationGroupId& replication_group_id, - const std::shared_ptr>& infos, - const TablegroupId& producer_tablegroup_id, const SetupReplicationInfo& setup_info, - const Status& s); - Status GetTablegroupSchemaCallbackInternal( - scoped_refptr& universe, - const std::vector& infos, const TablegroupId& producer_tablegroup_id, - const SetupReplicationInfo& setup_info, const Status& s); - void GetColocatedTabletSchemaCallback( - const xcluster::ReplicationGroupId& replication_group_id, - const std::shared_ptr>& info, - const SetupReplicationInfo& setup_info, const Status& s); - - typedef std::vector< - std::tuple>> - StreamUpdateInfos; - - void GetCDCStreamCallback( - const xrepl::StreamId& bootstrap_id, std::shared_ptr table_id, - std::shared_ptr> options, - const xcluster::ReplicationGroupId& replication_group_id, const TableId& table, - std::shared_ptr xcluster_rpc, const Status& s, - std::shared_ptr stream_update_infos, - std::shared_ptr update_infos_lock); - - void AddCDCStreamToUniverseAndInitConsumer( - const xcluster::ReplicationGroupId& replication_group_id, const TableId& table, - const Result& stream_id, std::function on_success_cb = nullptr); - - Status AddCDCStreamToUniverseAndInitConsumerInternal( - scoped_refptr universe, const TableId& table, - const xrepl::StreamId& stream_id, std::function on_success_cb); - - Status MergeUniverseReplication( - scoped_refptr info, xcluster::ReplicationGroupId original_id, - std::function on_success_cb); - - Status DeleteUniverseReplicationUnlocked(scoped_refptr info); - Status DeleteUniverseReplication( - const xcluster::ReplicationGroupId& replication_group_id, bool ignore_errors, - bool skip_producer_stream_deletion, DeleteUniverseReplicationResponsePB* resp); - - void MarkUniverseReplicationFailed( - scoped_refptr universe, const Status& failure_status); - // Sets the appropriate failure state and the error status on the universe and commits the - // mutation to the sys catalog. - void MarkUniverseReplicationFailed( - const Status& failure_status, CowWriteLock* universe_lock, - scoped_refptr universe); - - // Sets the appropriate state and on the replication bootstrap and commits the - // mutation to the sys catalog. - void SetReplicationBootstrapState( - scoped_refptr bootstrap_info, - const SysUniverseReplicationBootstrapEntryPB::State& state); - std::shared_ptr GetCDCServiceProxy( client::internal::RemoteTabletServer* ts); Result GetLeaderTServer( client::internal::RemoteTabletPtr tablet); - // Consumer API: Find out if bootstrap is required for the Producer tables. - Status IsBootstrapRequiredOnProducer( - scoped_refptr universe, - const TableId& producer_table, - const std::unordered_map& table_bootstrap_ids); - // Check if bootstrapping is required for a table. Status IsTableBootstrapRequired( const TableId& table_id, @@ -2912,9 +2736,6 @@ class CatalogManager : public tserver::TabletPeerLookupIf, std::unordered_set GetCDCSDKStreamsForTable(const TableId& table_id) const; - Status CreateTransactionAwareSnapshot( - const CreateSnapshotRequestPB& req, CreateSnapshotResponsePB* resp, CoarseTimePoint deadline); - Status CreateNonTransactionAwareSnapshot( const CreateSnapshotRequestPB* req, CreateSnapshotResponsePB* resp, const LeaderEpoch& epoch); @@ -2942,19 +2763,6 @@ class CatalogManager : public tserver::TabletPeerLookupIf, Result CollectEntriesForSequencesDataTable(); - Result> CreateUniverseReplicationInfoForProducer( - const xcluster::ReplicationGroupId& replication_group_id, - const google::protobuf::RepeatedPtrField& master_addresses, - const std::vector& producer_namespace_ids, - const std::vector& consumer_namespace_ids, - const google::protobuf::RepeatedPtrField& table_ids, bool transactional); - - Result> - CreateUniverseReplicationBootstrapInfoForProducer( - const xcluster::ReplicationGroupId& replication_group_id, - const google::protobuf::RepeatedPtrField& master_addresses, - const LeaderEpoch& epoch, bool transactional); - void ProcessXReplParentTabletDeletionPeriodically(); Status DoProcessCDCSDKTabletDeletion(); diff --git a/src/yb/master/master_replication_service.cc b/src/yb/master/master_replication_service.cc index 201ba5cf2563..c6befa687b74 100644 --- a/src/yb/master/master_replication_service.cc +++ b/src/yb/master/master_replication_service.cc @@ -30,22 +30,16 @@ class MasterReplicationServiceImpl : public MasterServiceBase, public MasterRepl MASTER_SERVICE_IMPL_ON_LEADER_WITH_LOCK( CatalogManager, (ValidateReplicationInfo) - (AlterUniverseReplication) (CreateCDCStream) (DeleteCDCStream) - (DeleteUniverseReplication) (GetCDCStream) (GetUniverseReplication) (GetUDTypeMetadata) - (IsSetupUniverseReplicationDone) - (IsSetupNamespaceReplicationWithBootstrapDone) (UpdateConsumerOnProducerSplit) (UpdateConsumerOnProducerMetadata) (ListCDCStreams) (IsObjectPartOfXRepl) (SetUniverseReplicationEnabled) - (SetupNamespaceReplicationWithBootstrap) - (SetupUniverseReplication) (UpdateCDCStream) (GetCDCDBStreamInfo) (IsBootstrapRequired) @@ -82,6 +76,12 @@ class MasterReplicationServiceImpl : public MasterServiceBase, public MasterRepl (GetUniverseReplicationInfo) (GetReplicationStatus) (XClusterReportNewAutoFlagConfigVersion) + (SetupUniverseReplication) + (IsSetupUniverseReplicationDone) + (SetupNamespaceReplicationWithBootstrap) + (IsSetupNamespaceReplicationWithBootstrapDone) + (AlterUniverseReplication) + (DeleteUniverseReplication) ) }; diff --git a/src/yb/master/xcluster/add_table_to_xcluster_target_task.cc b/src/yb/master/xcluster/add_table_to_xcluster_target_task.cc index 194849e023f5..f1025a8a0d52 100644 --- a/src/yb/master/xcluster/add_table_to_xcluster_target_task.cc +++ b/src/yb/master/xcluster/add_table_to_xcluster_target_task.cc @@ -168,7 +168,7 @@ Status AddTableToXClusterTargetTask::AddTableToReplicationGroup( req.set_replication_group_id(replication_group_id.ToString()); req.add_producer_table_ids_to_add(producer_table_id); req.add_producer_bootstrap_ids_to_add(bootstrap_id); - RETURN_NOT_OK(catalog_manager_.AlterUniverseReplication(&req, &resp, nullptr /* rpc */, epoch_)); + RETURN_NOT_OK(xcluster_manager_.AlterUniverseReplication(&req, &resp, nullptr /* rpc */, epoch_)); if (resp.has_error()) { return StatusFromPB(resp.error().status()); @@ -268,8 +268,7 @@ Status AddTableToXClusterTargetTask::WaitForXClusterSafeTimeCaughtUp() { Status AddTableToXClusterTargetTask::CleanupAndComplete() { // Ensure that we clean up the xcluster_source_table_id field. - RETURN_NOT_OK( - catalog_manager_.GetXClusterManager()->ClearXClusterSourceTableId(table_info_, epoch_)); + RETURN_NOT_OK(xcluster_manager_.ClearXClusterSourceTableId(table_info_, epoch_)); Complete(); return Status::OK(); diff --git a/src/yb/master/xcluster/xcluster_bootstrap_helper.cc b/src/yb/master/xcluster/xcluster_bootstrap_helper.cc new file mode 100644 index 000000000000..6c24f0f4f801 --- /dev/null +++ b/src/yb/master/xcluster/xcluster_bootstrap_helper.cc @@ -0,0 +1,461 @@ +// Copyright (c) YugabyteDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations +// under the License. +// + +#include "yb/master/xcluster/xcluster_bootstrap_helper.h" + +#include "yb/client/yb_table_name.h" +#include "yb/gutil/bind.h" +#include "yb/master/catalog_manager-internal.h" +#include "yb/master/catalog_manager.h" +#include "yb/master/snapshot_transfer_manager.h" +#include "yb/master/xcluster_rpc_tasks.h" +#include "yb/master/xcluster/xcluster_universe_replication_setup_helper.h" +#include "yb/util/backoff_waiter.h" + +using namespace std::literals; + +DEFINE_test_flag(bool, xcluster_fail_restore_consumer_snapshot, false, + "In the SetupReplicationWithBootstrap flow, test failure to restore snapshot on consumer."); + +// assumes the existence of a local variable bootstrap_info +#define MARK_BOOTSTRAP_FAILED_NOT_OK(s) \ + do { \ + auto&& _s = (s); \ + if (PREDICT_FALSE(!_s.ok())) { \ + MarkReplicationBootstrapFailed(bootstrap_info, _s); \ + return; \ + } \ + } while (false) + +#define VERIFY_RESULT_MARK_BOOTSTRAP_FAILED(expr) \ + RESULT_CHECKER_HELPER(expr, MARK_BOOTSTRAP_FAILED_NOT_OK(ResultToStatus(__result))) + +namespace yb::master { + +SetupUniverseReplicationWithBootstrapHelper::SetupUniverseReplicationWithBootstrapHelper( + Master& master, CatalogManager& catalog_manager, const LeaderEpoch& epoch) + : master_(master), + catalog_manager_(catalog_manager), + sys_catalog_(*catalog_manager.sys_catalog()), + xcluster_manager_(*catalog_manager.GetXClusterManagerImpl()), + epoch_(epoch) {} + +SetupUniverseReplicationWithBootstrapHelper::~SetupUniverseReplicationWithBootstrapHelper() {} + +Status SetupUniverseReplicationWithBootstrapHelper::SetupWithBootstrap( + Master& master, CatalogManager& catalog_manager, + const SetupNamespaceReplicationWithBootstrapRequestPB* req, + SetupNamespaceReplicationWithBootstrapResponsePB* resp, const LeaderEpoch& epoch) { + scoped_refptr helper = + new SetupUniverseReplicationWithBootstrapHelper(master, catalog_manager, epoch); + return helper->SetupWithBootstrap(req, resp); +} + +/* + * SetupNamespaceReplicationWithBootstrap is setup in 5 stages. + * 1. Validates user input & connect to producer. + * 2. Calls BootstrapProducer with all user tables in namespace. + * 3. Create snapshot on producer and import onto consumer. + * 4. Download snapshots from producer and restore on consumer. + * 5. SetupUniverseReplication. + */ +Status SetupUniverseReplicationWithBootstrapHelper::SetupWithBootstrap( + const SetupNamespaceReplicationWithBootstrapRequestPB* req, + SetupNamespaceReplicationWithBootstrapResponsePB* resp) { + // PHASE 1: Validating user input. + RETURN_NOT_OK(ValidateReplicationBootstrapRequest(req)); + + // Create entry in sys catalog. + auto replication_id = xcluster::ReplicationGroupId(req->replication_id()); + auto transactional = req->has_transactional() ? req->transactional() : false; + auto bootstrap_info = VERIFY_RESULT(CreateUniverseReplicationBootstrapInfoForProducer( + replication_id, req->producer_master_addresses(), transactional)); + + // Connect to producer. + auto xcluster_rpc_result = + bootstrap_info->GetOrCreateXClusterRpcTasks(req->producer_master_addresses()); + if (!xcluster_rpc_result.ok()) { + auto s = ResultToStatus(xcluster_rpc_result); + MarkReplicationBootstrapFailed(bootstrap_info, s); + return s; + } + auto xcluster_rpc_tasks = std::move(*xcluster_rpc_result); + + // Get user tables in producer namespace. + auto tables_result = xcluster_rpc_tasks->client()->ListUserTables(req->producer_namespace()); + if (!tables_result.ok()) { + auto s = ResultToStatus(tables_result); + MarkReplicationBootstrapFailed(bootstrap_info, s); + return s; + } + auto tables = std::move(*tables_result); + + // Bootstrap producer. + SetReplicationBootstrapState( + bootstrap_info, SysUniverseReplicationBootstrapEntryPB::BOOTSTRAP_PRODUCER); + auto s = xcluster_rpc_tasks->BootstrapProducer( + req->producer_namespace(), tables, + Bind( + &SetupUniverseReplicationWithBootstrapHelper::DoReplicationBootstrap, + scoped_refptr(this), replication_id, + tables)); + if (!s.ok()) { + MarkReplicationBootstrapFailed(bootstrap_info, s); + return s; + } + + return Status::OK(); +} +Result> +SetupUniverseReplicationWithBootstrapHelper::CreateUniverseReplicationBootstrapInfoForProducer( + const xcluster::ReplicationGroupId& replication_group_id, + const google::protobuf::RepeatedPtrField& master_addresses, bool transactional) { + if (catalog_manager_.GetUniverseReplicationBootstrap(replication_group_id) != nullptr) { + return STATUS( + InvalidArgument, Format("Bootstrap already present: $0", replication_group_id), + MasterError(MasterErrorPB::INVALID_REQUEST)); + } + + // Create an entry in the system catalog DocDB for this new universe replication. + scoped_refptr bootstrap_info = + new UniverseReplicationBootstrapInfo(replication_group_id); + bootstrap_info->mutable_metadata()->StartMutation(); + + SysUniverseReplicationBootstrapEntryPB* metadata = + &bootstrap_info->mutable_metadata()->mutable_dirty()->pb; + metadata->set_replication_group_id(replication_group_id.ToString()); + metadata->mutable_producer_master_addresses()->CopyFrom(master_addresses); + metadata->set_state(SysUniverseReplicationBootstrapEntryPB::INITIALIZING); + metadata->set_transactional(transactional); + metadata->set_leader_term(epoch_.leader_term); + metadata->set_pitr_count(epoch_.pitr_count); + + RETURN_NOT_OK(CheckLeaderStatus( + sys_catalog_.Upsert(epoch_, bootstrap_info), + "inserting universe replication bootstrap info into sys-catalog")); + + // Commit the in-memory state now that it's added to the persistent catalog. + bootstrap_info->mutable_metadata()->CommitMutation(); + LOG(INFO) << "Setup universe replication bootstrap from producer " << bootstrap_info->ToString(); + + catalog_manager_.InsertNewUniverseReplicationInfoBootstrapInfo(*bootstrap_info); + + return bootstrap_info; +} + +void SetupUniverseReplicationWithBootstrapHelper::MarkReplicationBootstrapFailed( + scoped_refptr bootstrap_info, const Status& failure_status) { + auto l = bootstrap_info->LockForWrite(); + MarkReplicationBootstrapFailed(failure_status, &l, bootstrap_info); +} + +void SetupUniverseReplicationWithBootstrapHelper::MarkReplicationBootstrapFailed( + const Status& failure_status, + CowWriteLock* bootstrap_info_lock, + scoped_refptr bootstrap_info) { + auto& l = *bootstrap_info_lock; + auto state = l->pb.state(); + if (state == SysUniverseReplicationBootstrapEntryPB::DELETED) { + l.mutable_data()->pb.set_state(SysUniverseReplicationBootstrapEntryPB::DELETED_ERROR); + } else { + l.mutable_data()->pb.set_state(SysUniverseReplicationBootstrapEntryPB::FAILED); + l.mutable_data()->pb.set_failed_on(state); + } + + LOG(WARNING) << Format( + "Replication bootstrap $0 failed: $1", bootstrap_info->ToString(), failure_status.ToString()); + + bootstrap_info->SetReplicationBootstrapErrorStatus(failure_status); + + // Update sys_catalog. + const Status s = sys_catalog_.Upsert(epoch_, bootstrap_info); + + l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); +} + +void SetupUniverseReplicationWithBootstrapHelper::SetReplicationBootstrapState( + scoped_refptr bootstrap_info, + const SysUniverseReplicationBootstrapEntryPB::State& state) { + auto l = bootstrap_info->LockForWrite(); + l.mutable_data()->set_state(state); + + // Update sys_catalog. + const Status s = sys_catalog_.Upsert(epoch_, bootstrap_info); + l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); +} + +Status SetupUniverseReplicationWithBootstrapHelper::ValidateReplicationBootstrapRequest( + const SetupNamespaceReplicationWithBootstrapRequestPB* req) { + SCHECK( + !req->replication_id().empty(), InvalidArgument, "Replication ID must be provided", + req->ShortDebugString()); + + SCHECK( + req->producer_master_addresses_size() > 0, InvalidArgument, + "Producer master address must be provided", req->ShortDebugString()); + + { + auto l = catalog_manager_.ClusterConfig()->LockForRead(); + SCHECK( + l->pb.cluster_uuid() != req->replication_id(), InvalidArgument, + "Replication name cannot be the target universe UUID", req->ShortDebugString()); + } + + RETURN_NOT_OK_PREPEND( + SetupUniverseReplicationHelper::ValidateMasterAddressesBelongToDifferentCluster( + master_, req->producer_master_addresses()), + req->ShortDebugString()); + + auto universe = + catalog_manager_.GetUniverseReplication(xcluster::ReplicationGroupId(req->replication_id())); + SCHECK( + universe == nullptr, InvalidArgument, + Format("Can't bootstrap replication that already exists")); + + return Status::OK(); +} + +void SetupUniverseReplicationWithBootstrapHelper::DoReplicationBootstrap( + const xcluster::ReplicationGroupId& replication_id, + const std::vector& tables, + Result bootstrap_producer_result) { + // First get the universe. + auto bootstrap_info = catalog_manager_.GetUniverseReplicationBootstrap(replication_id); + if (bootstrap_info == nullptr) { + LOG(ERROR) << "UniverseReplicationBootstrap not found: " << replication_id; + return; + } + + // Verify the result from BootstrapProducer & update values in PB if successful. + auto table_bootstrap_ids = + VERIFY_RESULT_MARK_BOOTSTRAP_FAILED(std::move(bootstrap_producer_result)); + { + auto l = bootstrap_info->LockForWrite(); + auto map = l.mutable_data()->pb.mutable_table_bootstrap_ids(); + for (const auto& [table_id, bootstrap_id] : table_bootstrap_ids) { + (*map)[table_id] = bootstrap_id.ToString(); + } + + // Update sys_catalog. + const Status s = sys_catalog_.Upsert(epoch_, bootstrap_info); + l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); + } + + // Create producer snapshot. + auto snapshot = VERIFY_RESULT_MARK_BOOTSTRAP_FAILED( + DoReplicationBootstrapCreateSnapshot(tables, bootstrap_info)); + + // Import snapshot and create consumer snapshot. + auto tables_meta = VERIFY_RESULT_MARK_BOOTSTRAP_FAILED( + DoReplicationBootstrapImportSnapshot(snapshot, bootstrap_info)); + + // Transfer and restore snapshot. + MARK_BOOTSTRAP_FAILED_NOT_OK( + DoReplicationBootstrapTransferAndRestoreSnapshot(tables_meta, bootstrap_info)); + + // Call SetupUniverseReplication + SetupUniverseReplicationRequestPB replication_req; + SetupUniverseReplicationResponsePB replication_resp; + { + auto l = bootstrap_info->LockForRead(); + replication_req.set_replication_group_id(l->pb.replication_group_id()); + replication_req.set_transactional(l->pb.transactional()); + replication_req.mutable_producer_master_addresses()->CopyFrom( + l->pb.producer_master_addresses()); + for (const auto& [table_id, bootstrap_id] : table_bootstrap_ids) { + replication_req.add_producer_table_ids(table_id); + replication_req.add_producer_bootstrap_ids(bootstrap_id.ToString()); + } + } + + SetReplicationBootstrapState( + bootstrap_info, SysUniverseReplicationBootstrapEntryPB::SETUP_REPLICATION); + MARK_BOOTSTRAP_FAILED_NOT_OK(SetupUniverseReplicationHelper::Setup( + master_, catalog_manager_, &replication_req, &replication_resp, epoch_)); + + LOG(INFO) << Format( + "Successfully completed replication bootstrap for $0", replication_id.ToString()); + SetReplicationBootstrapState(bootstrap_info, SysUniverseReplicationBootstrapEntryPB::DONE); +} + +Result +SetupUniverseReplicationWithBootstrapHelper::DoReplicationBootstrapCreateSnapshot( + const std::vector& tables, + scoped_refptr bootstrap_info) { + LOG(INFO) << Format( + "SetupReplicationWithBootstrap: create producer snapshot for replication $0", + bootstrap_info->id()); + SetReplicationBootstrapState( + bootstrap_info, SysUniverseReplicationBootstrapEntryPB::CREATE_PRODUCER_SNAPSHOT); + + auto xcluster_rpc_tasks = VERIFY_RESULT(bootstrap_info->GetOrCreateXClusterRpcTasks( + bootstrap_info->LockForRead()->pb.producer_master_addresses())); + + TxnSnapshotId old_snapshot_id = TxnSnapshotId::Nil(); + + // Send create request and wait for completion. + auto snapshot_result = xcluster_rpc_tasks->CreateSnapshot(tables, &old_snapshot_id); + + // If the producer failed to complete the snapshot, we still want to store the snapshot_id for + // cleanup purposes. + if (!old_snapshot_id.IsNil()) { + auto l = bootstrap_info->LockForWrite(); + l.mutable_data()->set_old_snapshot_id(old_snapshot_id); + + // Update sys_catalog. + const Status s = sys_catalog_.Upsert(epoch_, bootstrap_info); + l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); + } + + return snapshot_result; +} + +Result> +SetupUniverseReplicationWithBootstrapHelper::DoReplicationBootstrapImportSnapshot( + const SnapshotInfoPB& snapshot, + scoped_refptr bootstrap_info) { + /////////////////////////// + // ImportSnapshotMeta + /////////////////////////// + LOG(INFO) << Format( + "SetupReplicationWithBootstrap: import snapshot for replication $0", bootstrap_info->id()); + SetReplicationBootstrapState( + bootstrap_info, SysUniverseReplicationBootstrapEntryPB::IMPORT_SNAPSHOT); + + NamespaceMap namespace_map; + UDTypeMap type_map; + ExternalTableSnapshotDataMap tables_data; + + // ImportSnapshotMeta timeout should be a function of the table size. + auto deadline = CoarseMonoClock::Now() + MonoDelta::FromSeconds(10 + 1 * tables_data.size()); + auto epoch = bootstrap_info->LockForRead()->epoch(); + RETURN_NOT_OK(catalog_manager_.DoImportSnapshotMeta( + snapshot, epoch, std::nullopt /* clone_target_namespace_name */, &namespace_map, &type_map, + &tables_data, deadline)); + + // Update sys catalog with new information. + { + auto l = bootstrap_info->LockForWrite(); + l.mutable_data()->set_new_snapshot_objects(namespace_map, type_map, tables_data); + + // Update sys_catalog. + const Status s = sys_catalog_.Upsert(epoch_, bootstrap_info); + l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); + } + + /////////////////////////// + // CreateConsumerSnapshot + /////////////////////////// + LOG(INFO) << Format( + "SetupReplicationWithBootstrap: create consumer snapshot for replication $0", + bootstrap_info->id()); + SetReplicationBootstrapState( + bootstrap_info, SysUniverseReplicationBootstrapEntryPB::CREATE_CONSUMER_SNAPSHOT); + + CreateSnapshotRequestPB snapshot_req; + CreateSnapshotResponsePB snapshot_resp; + + std::vector tables_meta; + for (const auto& [table_id, table_data] : tables_data) { + if (table_data.table_meta) { + tables_meta.push_back(std::move(*table_data.table_meta)); + } + } + + for (const auto& table_meta : tables_meta) { + SCHECK( + ImportSnapshotMetaResponsePB_TableType_IsValid(table_meta.table_type()), InternalError, + Format("Found unknown table type: $0", table_meta.table_type())); + + const auto& new_table_id = table_meta.table_ids().new_id(); + RETURN_NOT_OK(catalog_manager_.WaitForCreateTableToFinish(new_table_id, deadline)); + + snapshot_req.mutable_tables()->Add()->set_table_id(new_table_id); + } + + snapshot_req.set_add_indexes(false); + snapshot_req.set_transaction_aware(true); + snapshot_req.set_imported(true); + RETURN_NOT_OK( + catalog_manager_.CreateTransactionAwareSnapshot(snapshot_req, &snapshot_resp, deadline)); + + // Update sys catalog with new information. + { + auto l = bootstrap_info->LockForWrite(); + l.mutable_data()->set_new_snapshot_id(TryFullyDecodeTxnSnapshotId(snapshot_resp.snapshot_id())); + + // Update sys_catalog. + const Status s = sys_catalog_.Upsert(epoch_, bootstrap_info); + l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); + } + + return std::vector(tables_meta.begin(), tables_meta.end()); +} + +Status +SetupUniverseReplicationWithBootstrapHelper::DoReplicationBootstrapTransferAndRestoreSnapshot( + const std::vector& tables_meta, + scoped_refptr bootstrap_info) { + // Retrieve required data from PB. + TxnSnapshotId old_snapshot_id = TxnSnapshotId::Nil(); + TxnSnapshotId new_snapshot_id = TxnSnapshotId::Nil(); + google::protobuf::RepeatedPtrField producer_masters; + auto epoch = bootstrap_info->epoch(); + { + auto l = bootstrap_info->LockForRead(); + old_snapshot_id = l->old_snapshot_id(); + new_snapshot_id = l->new_snapshot_id(); + producer_masters.CopyFrom(l->pb.producer_master_addresses()); + } + + auto xcluster_rpc_tasks = + VERIFY_RESULT(bootstrap_info->GetOrCreateXClusterRpcTasks(producer_masters)); + + // Transfer snapshot. + SetReplicationBootstrapState( + bootstrap_info, SysUniverseReplicationBootstrapEntryPB::TRANSFER_SNAPSHOT); + auto snapshot_transfer_manager = std::make_shared( + &master_, &catalog_manager_, xcluster_rpc_tasks->client()); + RETURN_NOT_OK_PREPEND( + snapshot_transfer_manager->TransferSnapshot( + old_snapshot_id, new_snapshot_id, tables_meta, epoch), + Format("Failed to transfer snapshot $0 from producer", old_snapshot_id.ToString())); + + // Restore snapshot. + SetReplicationBootstrapState( + bootstrap_info, SysUniverseReplicationBootstrapEntryPB::RESTORE_SNAPSHOT); + auto restoration_id = VERIFY_RESULT(catalog_manager_.snapshot_coordinator().Restore( + new_snapshot_id, HybridTime(), epoch.leader_term)); + + if (PREDICT_FALSE(FLAGS_TEST_xcluster_fail_restore_consumer_snapshot)) { + return STATUS(Aborted, "Test failure"); + } + + // Wait for restoration to complete. + return WaitFor( + [this, &new_snapshot_id, &restoration_id]() -> Result { + ListSnapshotRestorationsResponsePB resp; + RETURN_NOT_OK(catalog_manager_.snapshot_coordinator().ListRestorations( + restoration_id, new_snapshot_id, &resp)); + + SCHECK_EQ( + resp.restorations_size(), 1, IllegalState, + Format("Expected 1 restoration, got $0", resp.restorations_size())); + const auto& restoration = *resp.restorations().begin(); + const auto& state = restoration.entry().state(); + return state == SysSnapshotEntryPB::RESTORED; + }, + MonoDelta::kMax, "Waiting for restoration to finish", 100ms); +} + +} // namespace yb::master diff --git a/src/yb/master/xcluster/xcluster_bootstrap_helper.h b/src/yb/master/xcluster/xcluster_bootstrap_helper.h new file mode 100644 index 000000000000..116f77bf05b2 --- /dev/null +++ b/src/yb/master/xcluster/xcluster_bootstrap_helper.h @@ -0,0 +1,116 @@ +// Copyright (c) YugabyteDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations +// under the License. +// + +#pragma once + +#include "yb/cdc/xcluster_types.h" +#include "yb/master/leader_epoch.h" +#include "yb/master/master_fwd.h" +#include "yb/util/cow_object.h" +#include "yb/util/status_fwd.h" + +namespace yb { + +namespace rpc { +class RpcContext; +} // namespace rpc + +namespace client { +class YBTableName; +} // namespace client + +namespace master { + +class SetupNamespaceReplicationWithBootstrapRequestPB; +class SetupNamespaceReplicationWithBootstrapResponsePB; +class UniverseReplicationBootstrapInfo; +struct PersistentUniverseReplicationBootstrapInfo; + +// NOTE: +// SetupUniverseReplicationWithBootstrap is currently not in use. There are no tests that cover +// this, so it should be assumed to have regressions. This code is just kept as a refences for +// future use. Use caution when copying and reusing part of this code. + +class SetupUniverseReplicationWithBootstrapHelper + : public RefCountedThreadSafe { + public: + ~SetupUniverseReplicationWithBootstrapHelper(); + + static Status SetupWithBootstrap( + Master& master, CatalogManager& catalog_manager, + const SetupNamespaceReplicationWithBootstrapRequestPB* req, + SetupNamespaceReplicationWithBootstrapResponsePB* resp, const LeaderEpoch& epoch); + + private: + SetupUniverseReplicationWithBootstrapHelper( + Master& master, CatalogManager& catalog_manager, const LeaderEpoch& epoch); + + Status SetupWithBootstrap( + const SetupNamespaceReplicationWithBootstrapRequestPB* req, + SetupNamespaceReplicationWithBootstrapResponsePB* resp); + + Result> + CreateUniverseReplicationBootstrapInfoForProducer( + const xcluster::ReplicationGroupId& replication_group_id, + const google::protobuf::RepeatedPtrField& master_addresses, bool transactional); + + void MarkReplicationBootstrapFailed( + scoped_refptr bootstrap_info, const Status& failure_status); + // Sets the appropriate failure state and the error status on the replication bootstrap and + // commits the mutation to the sys catalog. + void MarkReplicationBootstrapFailed( + const Status& failure_status, + CowWriteLock* bootstrap_info_lock, + scoped_refptr bootstrap_info); + + // Sets the appropriate state and on the replication bootstrap and commits the + // mutation to the sys catalog. + void SetReplicationBootstrapState( + scoped_refptr bootstrap_info, + const SysUniverseReplicationBootstrapEntryPB::State& state); + + // SetupReplicationWithBootstrap + Status ValidateReplicationBootstrapRequest( + const SetupNamespaceReplicationWithBootstrapRequestPB* req); + + typedef std::unordered_map TableBootstrapIdsMap; + void DoReplicationBootstrap( + const xcluster::ReplicationGroupId& replication_id, + const std::vector& tables, + Result bootstrap_producer_result); + + Result DoReplicationBootstrapCreateSnapshot( + const std::vector& tables, + scoped_refptr bootstrap_info); + + Result> + DoReplicationBootstrapImportSnapshot( + const SnapshotInfoPB& snapshot, + scoped_refptr bootstrap_info); + + Status DoReplicationBootstrapTransferAndRestoreSnapshot( + const std::vector& tables_meta, + scoped_refptr bootstrap_info); + + Master& master_; + CatalogManager& catalog_manager_; + SysCatalogTable& sys_catalog_; + XClusterManager& xcluster_manager_; + const LeaderEpoch epoch_; + + DISALLOW_COPY_AND_ASSIGN(SetupUniverseReplicationWithBootstrapHelper); +}; + +} // namespace master + +} // namespace yb diff --git a/src/yb/master/xcluster/xcluster_manager.cc b/src/yb/master/xcluster/xcluster_manager.cc index 74ca3fcdfbf8..e99aeb9d4734 100644 --- a/src/yb/master/xcluster/xcluster_manager.cc +++ b/src/yb/master/xcluster/xcluster_manager.cc @@ -46,6 +46,23 @@ DEFINE_RUNTIME_AUTO_bool(enable_tablet_split_of_xcluster_replicated_tables, kExt namespace yb::master { +namespace { + +template +Status ValidateUniverseUUID(const RequestType& req, CatalogManager& catalog_manager) { + if (req->has_universe_uuid() && !req->universe_uuid().empty()) { + auto universe_uuid = catalog_manager.GetUniverseUuidIfExists(); + SCHECK( + universe_uuid && universe_uuid->ToString() == req->universe_uuid(), InvalidArgument, + "Invalid Universe UUID $0. Expected $1", req->universe_uuid(), + (universe_uuid ? universe_uuid->ToString() : "empty")); + } + + return Status::OK(); +} + +} // namespace + XClusterManager::XClusterManager( Master& master, CatalogManager& catalog_manager, SysCatalogTable& sys_catalog) : XClusterSourceManager(master, catalog_manager, sys_catalog), @@ -677,4 +694,86 @@ Status XClusterManager::HandleTabletSchemaVersionReport( table_info, consumer_schema_version, epoch); } +Status XClusterManager::SetupUniverseReplication( + const SetupUniverseReplicationRequestPB* req, SetupUniverseReplicationResponsePB* resp, + rpc::RpcContext* rpc, const LeaderEpoch& epoch) { + LOG_FUNC_AND_RPC; + + return XClusterTargetManager::SetupUniverseReplication(req, resp, epoch); +} + +/* + * Checks if the universe replication setup has completed. + * Returns Status::OK() if this call succeeds, and uses resp->done() to determine if the setup has + * completed (either failed or succeeded). If the setup has failed, then resp->replication_error() + * is also set. If it succeeds, replication_error() gets set to OK. + */ +Status XClusterManager::IsSetupUniverseReplicationDone( + const IsSetupUniverseReplicationDoneRequestPB* req, + IsSetupUniverseReplicationDoneResponsePB* resp, rpc::RpcContext* rpc) { + LOG_FUNC_AND_RPC; + + SCHECK_PB_FIELDS_NOT_EMPTY(*req, replication_group_id); + + auto is_operation_done = VERIFY_RESULT(XClusterTargetManager::IsSetupUniverseReplicationDone( + xcluster::ReplicationGroupId(req->replication_group_id()))); + + resp->set_done(is_operation_done.done()); + StatusToPB(is_operation_done.status(), resp->mutable_replication_error()); + return Status::OK(); +} + +Status XClusterManager::SetupNamespaceReplicationWithBootstrap( + const SetupNamespaceReplicationWithBootstrapRequestPB* req, + SetupNamespaceReplicationWithBootstrapResponsePB* resp, rpc::RpcContext* rpc, + const LeaderEpoch& epoch) { + LOG_FUNC_AND_RPC; + + return XClusterTargetManager::SetupNamespaceReplicationWithBootstrap(req, resp, epoch); +} + +Status XClusterManager::IsSetupNamespaceReplicationWithBootstrapDone( + const IsSetupNamespaceReplicationWithBootstrapDoneRequestPB* req, + IsSetupNamespaceReplicationWithBootstrapDoneResponsePB* resp, rpc::RpcContext* rpc) { + LOG_FUNC_AND_RPC; + + SCHECK_PB_FIELDS_NOT_EMPTY(*req, replication_group_id); + + *resp = VERIFY_RESULT(XClusterTargetManager::IsSetupNamespaceReplicationWithBootstrapDone( + xcluster::ReplicationGroupId(req->replication_group_id()))); + + return Status::OK(); +} + +Status XClusterManager::AlterUniverseReplication( + const AlterUniverseReplicationRequestPB* req, AlterUniverseReplicationResponsePB* resp, + rpc::RpcContext* rpc, const LeaderEpoch& epoch) { + LOG_FUNC_AND_RPC; + + SCHECK_PB_FIELDS_NOT_EMPTY(*req, replication_group_id); + + RETURN_NOT_OK(ValidateUniverseUUID(req, catalog_manager_)); + + return XClusterTargetManager::AlterUniverseReplication(req, resp, epoch); +} + +Status XClusterManager::DeleteUniverseReplication( + const DeleteUniverseReplicationRequestPB* req, DeleteUniverseReplicationResponsePB* resp, + rpc::RpcContext* rpc, const LeaderEpoch& epoch) { + LOG_FUNC_AND_RPC; + + SCHECK_PB_FIELDS_NOT_EMPTY(*req, replication_group_id); + + RETURN_NOT_OK(ValidateUniverseUUID(req, catalog_manager_)); + + RETURN_NOT_OK(XClusterTargetManager::DeleteUniverseReplication( + xcluster::ReplicationGroupId(req->replication_group_id()), req->ignore_errors(), + req->skip_producer_stream_deletion(), resp, epoch)); + + LOG(INFO) << "Successfully completed DeleteUniverseReplication request from " + << RequestorString(rpc); + + return Status::OK(); +} + } // namespace yb::master diff --git a/src/yb/master/xcluster/xcluster_manager.h b/src/yb/master/xcluster/xcluster_manager.h index 665fc66e1c64..60c51867b373 100644 --- a/src/yb/master/xcluster/xcluster_manager.h +++ b/src/yb/master/xcluster/xcluster_manager.h @@ -101,6 +101,31 @@ class XClusterManager : public XClusterManagerIf, Status RefreshXClusterSafeTimeMap(const LeaderEpoch& epoch) override; + Status SetupUniverseReplication( + const SetupUniverseReplicationRequestPB* req, SetupUniverseReplicationResponsePB* resp, + rpc::RpcContext* rpc, const LeaderEpoch& epoch); + + Status IsSetupUniverseReplicationDone( + const IsSetupUniverseReplicationDoneRequestPB* req, + IsSetupUniverseReplicationDoneResponsePB* resp, rpc::RpcContext* rpc); + + Status SetupNamespaceReplicationWithBootstrap( + const SetupNamespaceReplicationWithBootstrapRequestPB* req, + SetupNamespaceReplicationWithBootstrapResponsePB* resp, rpc::RpcContext* rpc, + const LeaderEpoch& epoch); + + Status IsSetupNamespaceReplicationWithBootstrapDone( + const IsSetupNamespaceReplicationWithBootstrapDoneRequestPB* req, + IsSetupNamespaceReplicationWithBootstrapDoneResponsePB* resp, rpc::RpcContext* rpc); + + Status AlterUniverseReplication( + const AlterUniverseReplicationRequestPB* req, AlterUniverseReplicationResponsePB* resp, + rpc::RpcContext* rpc, const LeaderEpoch& epoch) override; + + Status DeleteUniverseReplication( + const DeleteUniverseReplicationRequestPB* req, DeleteUniverseReplicationResponsePB* resp, + rpc::RpcContext* rpc, const LeaderEpoch& epoch); + // OutboundReplicationGroup RPCs. Status XClusterCreateOutboundReplicationGroup( const XClusterCreateOutboundReplicationGroupRequestPB* req, @@ -198,7 +223,7 @@ class XClusterManager : public XClusterManagerIf, const xrepl::StreamId& stream_id); void RemoveTableConsumerStream( - const TableId& table_id, const xcluster::ReplicationGroupId& replication_group_id); + const TableId& table_id, const xcluster::ReplicationGroupId& replication_group_id) override; void RemoveTableConsumerStreams( const xcluster::ReplicationGroupId& replication_group_id, diff --git a/src/yb/master/xcluster/xcluster_manager_if.h b/src/yb/master/xcluster/xcluster_manager_if.h index 2dfdc81c836e..1d7d696ac8d7 100644 --- a/src/yb/master/xcluster/xcluster_manager_if.h +++ b/src/yb/master/xcluster/xcluster_manager_if.h @@ -86,6 +86,13 @@ class XClusterManagerIf { const TableId& consumer_table_id, const SplitTabletIds& split_tablet_ids, const LeaderEpoch& epoch) = 0; + virtual void RemoveTableConsumerStream( + const TableId& table_id, const xcluster::ReplicationGroupId& replication_group_id) = 0; + + virtual Status AlterUniverseReplication( + const AlterUniverseReplicationRequestPB* req, AlterUniverseReplicationResponsePB* resp, + rpc::RpcContext* rpc, const LeaderEpoch& epoch) = 0; + protected: virtual ~XClusterManagerIf() = default; }; diff --git a/src/yb/master/xcluster/xcluster_replication_group.cc b/src/yb/master/xcluster/xcluster_replication_group.cc index 2545bf823e50..2b47c7bd572e 100644 --- a/src/yb/master/xcluster/xcluster_replication_group.cc +++ b/src/yb/master/xcluster/xcluster_replication_group.cc @@ -18,45 +18,27 @@ #include "yb/client/xcluster_client.h" #include "yb/common/wire_protocol.pb.h" #include "yb/master/catalog_entity_info.h" +#include "yb/master/catalog_manager-internal.h" #include "yb/master/catalog_manager.h" #include "yb/master/catalog_manager_util.h" #include "yb/master/xcluster/master_xcluster_util.h" +#include "yb/util/flags/auto_flags_util.h" #include "yb/util/is_operation_done_result.h" #include "yb/master/xcluster_rpc_tasks.h" #include "yb/master/xcluster/xcluster_manager_if.h" #include "yb/common/xcluster_util.h" #include "yb/master/sys_catalog.h" -#include "yb/util/flags/auto_flags_util.h" #include "yb/util/result.h" DEFINE_RUNTIME_bool(xcluster_skip_health_check_on_replication_setup, false, "Skip health check on xCluster replication setup"); +DEFINE_test_flag(bool, exit_unfinished_deleting, false, + "Whether to exit part way through the deleting universe process."); + namespace yb::master { namespace { -// Returns nullopt when source universe does not support AutoFlags compatibility check. -// Returns a pair of bool which indicates if the configs are compatible and the source universe -// AutoFlags config version. -Result>> ValidateAutoFlagsConfig( - UniverseReplicationInfo& replication_info, const AutoFlagsConfigPB& local_config) { - auto master_addresses = replication_info.LockForRead()->pb.producer_master_addresses(); - auto xcluster_rpc = VERIFY_RESULT(replication_info.GetOrCreateXClusterRpcTasks(master_addresses)); - auto result = VERIFY_RESULT( - xcluster_rpc->client()->ValidateAutoFlagsConfig(local_config, AutoFlagClass::kExternal)); - - if (!result) { - return std::nullopt; - } - - auto& [is_valid, source_version] = *result; - VLOG(2) << "ValidateAutoFlagsConfig for replication group: " - << replication_info.ReplicationGroupId() << ", is_valid: " << is_valid - << ", source universe version: " << source_version - << ", target universe version: " << local_config.config_version(); - - return result; -} Result GetProducerEntry( SysClusterConfigEntryPB& cluster_config_pb, @@ -182,32 +164,40 @@ Result IsReplicationGroupReady( return IsSafeTimeReady(l->pb, xcluster_manager); } +Status ReturnErrorOrAddWarning( + const Status& s, bool ignore_errors, DeleteUniverseReplicationResponsePB* resp) { + if (!s.ok()) { + if (ignore_errors) { + // Continue executing, save the status as a warning. + AppStatusPB* warning = resp->add_warnings(); + StatusToPB(s, warning); + return Status::OK(); + } + return s.CloneAndAppend("\nUse 'ignore-errors' to ignore this error."); + } + return s; +} + } // namespace -Result GetAutoFlagConfigVersionIfCompatible( +Result>> ValidateAutoFlagsConfig( UniverseReplicationInfo& replication_info, const AutoFlagsConfigPB& local_config) { - const auto& replication_group_id = replication_info.ReplicationGroupId(); - - VLOG_WITH_FUNC(2) << "Validating AutoFlags config for replication group: " << replication_group_id - << " with target config version: " << local_config.config_version(); - - auto validate_result = VERIFY_RESULT(ValidateAutoFlagsConfig(replication_info, local_config)); + auto master_addresses = replication_info.LockForRead()->pb.producer_master_addresses(); + auto xcluster_rpc = VERIFY_RESULT(replication_info.GetOrCreateXClusterRpcTasks(master_addresses)); + auto result = VERIFY_RESULT( + xcluster_rpc->client()->ValidateAutoFlagsConfig(local_config, AutoFlagClass::kExternal)); - if (!validate_result) { - VLOG_WITH_FUNC(2) - << "Source universe of replication group " << replication_group_id - << " is running a version that does not support the AutoFlags compatibility check yet"; - return kInvalidAutoFlagsConfigVersion; + if (!result) { + return std::nullopt; } - auto& [is_valid, source_version] = *validate_result; - - SCHECK( - is_valid, IllegalState, - "AutoFlags between the universes are not compatible. Upgrade the target universe to a " - "version higher than or equal to the source universe"); + auto& [is_valid, source_version] = *result; + VLOG(2) << "ValidateAutoFlagsConfig for replication group: " + << replication_info.ReplicationGroupId() << ", is_valid: " << is_valid + << ", source universe version: " << source_version + << ", target universe version: " << local_config.config_version(); - return source_version; + return result; } Status RefreshAutoFlagConfigVersion( @@ -610,43 +600,6 @@ Status RemoveTablesFromReplicationGroupInternal( return Status::OK(); } -Status ValidateTableListForDbScopedReplication( - UniverseReplicationInfo& universe, const std::vector& namespace_ids, - const std::set& replicated_tables, const CatalogManager& catalog_manager) { - std::set validated_tables; - - for (const auto& namespace_id : namespace_ids) { - auto table_infos = - VERIFY_RESULT(GetTablesEligibleForXClusterReplication(catalog_manager, namespace_id)); - - std::vector missing_tables; - - for (const auto& table_info : table_infos) { - const auto& table_id = table_info->id(); - if (replicated_tables.contains(table_id)) { - validated_tables.insert(table_id); - } else { - missing_tables.push_back(table_id); - } - } - - SCHECK_FORMAT( - missing_tables.empty(), IllegalState, - "Namespace $0 has additional tables that were not added to xCluster DB Scoped replication " - "group $1: $2", - namespace_id, universe.id(), yb::ToString(missing_tables)); - } - - auto diff = STLSetSymmetricDifference(replicated_tables, validated_tables); - SCHECK_FORMAT( - diff.empty(), IllegalState, - "xCluster DB Scoped replication group $0 contains tables $1 that do not belong to replicated " - "namespaces $2", - universe.id(), yb::ToString(diff), yb::ToString(namespace_ids)); - - return Status::OK(); -} - bool HasNamespace(UniverseReplicationInfo& universe, const NamespaceId& consumer_namespace_id) { auto l = universe.LockForRead(); if (!l->IsDbScoped()) { @@ -661,4 +614,120 @@ bool HasNamespace(UniverseReplicationInfo& universe, const NamespaceId& consumer }); } +Status DeleteUniverseReplication( + UniverseReplicationInfo& universe, bool ignore_errors, bool skip_producer_stream_deletion, + DeleteUniverseReplicationResponsePB* resp, CatalogManager& catalog_manager, + const LeaderEpoch& epoch) { + const auto& replication_group_id = universe.ReplicationGroupId(); + auto xcluster_manager = catalog_manager.GetXClusterManager(); + auto sys_catalog = catalog_manager.sys_catalog(); + + { + auto l = universe.LockForWrite(); + l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::DELETING); + Status s = sys_catalog->Upsert(epoch, &universe); + RETURN_NOT_OK( + CheckLeaderStatus(s, "Updating delete universe replication info into sys-catalog")); + l.Commit(); + } + + auto l = universe.LockForWrite(); + l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::DELETED); + + // We can skip the deletion of individual streams for DB Scoped replication since deletion of the + // outbound replication group will clean it up. + if (l->IsDbScoped()) { + skip_producer_stream_deletion = true; + } + + // Delete subscribers on the Consumer Registry (removes from TServers). + LOG(INFO) << "Deleting subscribers for producer " << replication_group_id; + { + auto cluster_config = catalog_manager.ClusterConfig(); + auto cl = cluster_config->LockForWrite(); + auto* consumer_registry = cl.mutable_data()->pb.mutable_consumer_registry(); + auto replication_group_map = consumer_registry->mutable_producer_map(); + auto it = replication_group_map->find(replication_group_id.ToString()); + if (it != replication_group_map->end()) { + replication_group_map->erase(it); + cl.mutable_data()->pb.set_version(cl.mutable_data()->pb.version() + 1); + RETURN_NOT_OK(CheckStatus( + sys_catalog->Upsert(epoch, cluster_config.get()), + "updating cluster config in sys-catalog")); + + xcluster_manager->SyncConsumerReplicationStatusMap( + replication_group_id, *replication_group_map); + cl.Commit(); + } + } + + // Delete CDC stream config on the Producer. + if (!l->pb.table_streams().empty() && !skip_producer_stream_deletion) { + auto result = universe.GetOrCreateXClusterRpcTasks(l->pb.producer_master_addresses()); + if (!result.ok()) { + LOG(WARNING) << "Unable to create cdc rpc task. CDC streams won't be deleted: " << result; + } else { + auto xcluster_rpc = *result; + std::vector streams; + std::unordered_map stream_to_producer_table_id; + for (const auto& [table_id, stream_id_str] : l->pb.table_streams()) { + auto stream_id = VERIFY_RESULT(xrepl::StreamId::FromString(stream_id_str)); + streams.emplace_back(stream_id); + stream_to_producer_table_id.emplace(stream_id, table_id); + } + + DeleteCDCStreamResponsePB delete_cdc_stream_resp; + // Set force_delete=true since we are deleting active xCluster streams. + // Since we are deleting universe replication, we should be ok with + // streams not existing on the other side, so we pass in ignore_errors + bool ignore_missing_streams = false; + auto s = xcluster_rpc->client()->DeleteCDCStream( + streams, true, /* force_delete */ + true /* ignore_errors */, &delete_cdc_stream_resp); + + if (delete_cdc_stream_resp.not_found_stream_ids().size() > 0) { + std::vector missing_streams; + missing_streams.reserve(delete_cdc_stream_resp.not_found_stream_ids().size()); + for (const auto& stream_id : delete_cdc_stream_resp.not_found_stream_ids()) { + missing_streams.emplace_back(Format( + "$0 (table_id: $1)", stream_id, + stream_to_producer_table_id[VERIFY_RESULT(xrepl::StreamId::FromString(stream_id))])); + } + auto message = + Format("Could not find the following streams: $0.", AsString(missing_streams)); + + if (s.ok()) { + // Returned but did not find some streams, so still need to warn the user about those. + ignore_missing_streams = true; + s = STATUS(NotFound, message); + } else { + s = s.CloneAndPrepend(message); + } + } + RETURN_NOT_OK(ReturnErrorOrAddWarning(s, ignore_errors | ignore_missing_streams, resp)); + } + } + + if (PREDICT_FALSE(FLAGS_TEST_exit_unfinished_deleting)) { + // Exit for testing services + return Status::OK(); + } + + // Delete universe in the Universe Config. + RETURN_NOT_OK( + ReturnErrorOrAddWarning(sys_catalog->Delete(epoch, &universe), ignore_errors, resp)); + + // Also update the mapping of consumer tables. + for (const auto& table : universe.metadata().state().pb.validated_tables()) { + xcluster_manager->RemoveTableConsumerStream(table.second, universe.ReplicationGroupId()); + } + + catalog_manager.RemoveUniverseReplicationFromMap(universe.ReplicationGroupId()); + + l.Commit(); + LOG(INFO) << "Processed delete universe replication of " << universe.ToString(); + + return Status::OK(); +} + } // namespace yb::master diff --git a/src/yb/master/xcluster/xcluster_replication_group.h b/src/yb/master/xcluster/xcluster_replication_group.h index d37d9c93a53f..066056394e05 100644 --- a/src/yb/master/xcluster/xcluster_replication_group.h +++ b/src/yb/master/xcluster/xcluster_replication_group.h @@ -31,12 +31,10 @@ namespace master { // TODO: #19714 Create XClusterReplicationGroup, a wrapper over UniverseReplicationInfo, that will // manage the ReplicationGroup and its ProducerEntryPB in ClusterConfigInfo. -// Check if the local AutoFlags config is compatible with the source universe and returns the source -// universe AutoFlags config version if they are compatible. -// If they are not compatible, returns a bad status. -// If the source universe is running an older version which does not support AutoFlags compatiblity -// check, returns an invalid AutoFlags config version. -Result GetAutoFlagConfigVersionIfCompatible( +// Returns nullopt when source universe does not support AutoFlags compatibility check. +// Returns a pair with a bool which indicates if the configs are compatible and the source universe +// AutoFlags config version. +Result>> ValidateAutoFlagsConfig( UniverseReplicationInfo& replication_info, const AutoFlagsConfigPB& local_config); // Reruns the AutoFlags compatiblity validation when source universe AutoFlags config version has @@ -94,12 +92,13 @@ Status RemoveNamespaceFromReplicationGroup( scoped_refptr universe, const NamespaceId& producer_namespace_id, CatalogManager& catalog_manager, const LeaderEpoch& epoch); -Status ValidateTableListForDbScopedReplication( - UniverseReplicationInfo& universe, const std::vector& namespace_ids, - const std::set& replicated_table_ids, const CatalogManager& catalog_manager); - // Returns true if the namespace is part of the DB Scoped replication group. bool HasNamespace(UniverseReplicationInfo& universe, const NamespaceId& consumer_namespace_id); +Status DeleteUniverseReplication( + UniverseReplicationInfo& universe, bool ignore_errors, bool skip_producer_stream_deletion, + DeleteUniverseReplicationResponsePB* resp, CatalogManager& catalog_manager, + const LeaderEpoch& epoch); + } // namespace master } // namespace yb diff --git a/src/yb/master/xcluster/xcluster_target_manager.cc b/src/yb/master/xcluster/xcluster_target_manager.cc index b46b461eacc4..85b90b724c36 100644 --- a/src/yb/master/xcluster/xcluster_target_manager.cc +++ b/src/yb/master/xcluster/xcluster_target_manager.cc @@ -18,16 +18,20 @@ #include "yb/master/catalog_entity_info.h" #include "yb/master/catalog_manager.h" #include "yb/master/master.h" + +#include "yb/master/xcluster_consumer_registry_service.h" #include "yb/master/xcluster/add_table_to_xcluster_target_task.h" #include "yb/master/xcluster/master_xcluster_util.h" +#include "yb/master/xcluster/xcluster_bootstrap_helper.h" #include "yb/master/xcluster/xcluster_replication_group.h" #include "yb/master/xcluster/xcluster_safe_time_service.h" - #include "yb/master/xcluster/xcluster_status.h" -#include "yb/master/xcluster_consumer_registry_service.h" -#include "yb/util/jsonwriter.h" +#include "yb/master/xcluster/xcluster_universe_replication_alter_helper.h" +#include "yb/master/xcluster/xcluster_universe_replication_setup_helper.h" + #include "yb/util/backoff_waiter.h" #include "yb/util/is_operation_done_result.h" +#include "yb/util/jsonwriter.h" #include "yb/util/scope_exit.h" #include "yb/util/status.h" @@ -235,7 +239,7 @@ Status XClusterTargetManager::WaitForSetupUniverseReplicationToFinish( return Wait( [&]() -> Result { auto is_operation_done = - VERIFY_RESULT(IsSetupUniverseReplicationDone(replication_group_id, catalog_manager_)); + VERIFY_RESULT(IsSetupUniverseReplicationDone(replication_group_id)); if (is_operation_done.done()) { RETURN_NOT_OK(is_operation_done.status()); @@ -1101,4 +1105,82 @@ Status XClusterTargetManager::ProcessPendingSchemaChanges(const LeaderEpoch& epo return Status::OK(); } +Status XClusterTargetManager::SetupUniverseReplication( + const SetupUniverseReplicationRequestPB* req, SetupUniverseReplicationResponsePB* resp, + const LeaderEpoch& epoch) { + return SetupUniverseReplicationHelper::Setup(master_, catalog_manager_, req, resp, epoch); +} + +Result XClusterTargetManager::IsSetupUniverseReplicationDone( + const xcluster::ReplicationGroupId& replication_group_id) { + return master::IsSetupUniverseReplicationDone(replication_group_id, catalog_manager_); +} + +Status XClusterTargetManager::SetupNamespaceReplicationWithBootstrap( + const SetupNamespaceReplicationWithBootstrapRequestPB* req, + SetupNamespaceReplicationWithBootstrapResponsePB* resp, const LeaderEpoch& epoch) { + return SetupUniverseReplicationWithBootstrapHelper::SetupWithBootstrap( + master_, catalog_manager_, req, resp, epoch); +} + +Result +XClusterTargetManager::IsSetupNamespaceReplicationWithBootstrapDone( + const xcluster::ReplicationGroupId& replication_group_id) { + auto bootstrap_info = catalog_manager_.GetUniverseReplicationBootstrap(replication_group_id); + SCHECK( + bootstrap_info != nullptr, NotFound, + Format("Could not find universe replication bootstrap $0", replication_group_id)); + + IsSetupNamespaceReplicationWithBootstrapDoneResponsePB resp; + + // Terminal states are DONE or some failure state. + auto l = bootstrap_info->LockForRead(); + resp.set_state(l->state()); + + if (l->is_done()) { + resp.set_done(true); + StatusToPB(Status::OK(), resp.mutable_bootstrap_error()); + } else if (l->is_deleted_or_failed()) { + resp.set_done(true); + + if (!bootstrap_info->GetReplicationBootstrapErrorStatus().ok()) { + StatusToPB( + bootstrap_info->GetReplicationBootstrapErrorStatus(), resp.mutable_bootstrap_error()); + } else { + LOG(WARNING) << "Did not find setup universe replication bootstrap error status."; + StatusToPB(STATUS(InternalError, "unknown error"), resp.mutable_bootstrap_error()); + } + + // Add failed bootstrap to GC now that we've responded to the user. + catalog_manager_.MarkReplicationBootstrapForCleanup(bootstrap_info->ReplicationGroupId()); + } else { + // Not done yet. + resp.set_done(false); + } + + return resp; +} + +Status XClusterTargetManager::AlterUniverseReplication( + const AlterUniverseReplicationRequestPB* req, AlterUniverseReplicationResponsePB* resp, + const LeaderEpoch& epoch) { + return AlterUniverseReplicationHelper::Alter(master_, catalog_manager_, req, resp, epoch); +} + +Status XClusterTargetManager::DeleteUniverseReplication( + const xcluster::ReplicationGroupId& replication_group_id, bool ignore_errors, + bool skip_producer_stream_deletion, DeleteUniverseReplicationResponsePB* resp, + const LeaderEpoch& epoch) { + auto ri = catalog_manager_.GetUniverseReplication(replication_group_id); + SCHECK(ri != nullptr, NotFound, "Universe replication $0 does not exist", replication_group_id); + + RETURN_NOT_OK(master::DeleteUniverseReplication( + *ri, ignore_errors, skip_producer_stream_deletion, resp, catalog_manager_, epoch)); + + // Run the safe time task as it may need to perform cleanups of it own + CreateXClusterSafeTimeTableAndStartService(); + + return Status::OK(); +} + } // namespace yb::master diff --git a/src/yb/master/xcluster/xcluster_target_manager.h b/src/yb/master/xcluster/xcluster_target_manager.h index 22132effd1e6..93ce74705f9f 100644 --- a/src/yb/master/xcluster/xcluster_target_manager.h +++ b/src/yb/master/xcluster/xcluster_target_manager.h @@ -18,6 +18,7 @@ #include "yb/master/xcluster/master_xcluster_types.h" #include "yb/master/xcluster/xcluster_catalog_entity.h" +#include "yb/util/is_operation_done_result.h" #include "yb/util/status_fwd.h" namespace yb::master { @@ -166,6 +167,30 @@ class XClusterTargetManager { const TableInfo& table_info, SchemaVersion consumer_schema_version, const LeaderEpoch& epoch) EXCLUDES(table_stream_ids_map_mutex_); + Status SetupUniverseReplication( + const SetupUniverseReplicationRequestPB* req, SetupUniverseReplicationResponsePB* resp, + const LeaderEpoch& epoch); + + Result IsSetupUniverseReplicationDone( + const xcluster::ReplicationGroupId& replication_group_id); + + Status SetupNamespaceReplicationWithBootstrap( + const SetupNamespaceReplicationWithBootstrapRequestPB* req, + SetupNamespaceReplicationWithBootstrapResponsePB* resp, const LeaderEpoch& epoch); + + Result + IsSetupNamespaceReplicationWithBootstrapDone( + const xcluster::ReplicationGroupId& replication_group_id); + + Status AlterUniverseReplication( + const AlterUniverseReplicationRequestPB* req, AlterUniverseReplicationResponsePB* resp, + const LeaderEpoch& epoch); + + Status DeleteUniverseReplication( + const xcluster::ReplicationGroupId& replication_group_id, bool ignore_errors, + bool skip_producer_stream_deletion, DeleteUniverseReplicationResponsePB* resp, + const LeaderEpoch& epoch); + private: // Gets the replication group status for the given replication group id. Does not populate the // table statuses. diff --git a/src/yb/master/xcluster/xcluster_universe_replication_alter_helper.cc b/src/yb/master/xcluster/xcluster_universe_replication_alter_helper.cc new file mode 100644 index 000000000000..b5cb87b86a7a --- /dev/null +++ b/src/yb/master/xcluster/xcluster_universe_replication_alter_helper.cc @@ -0,0 +1,318 @@ +// Copyright (c) YugabyteDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations +// under the License. +// + +#include "yb/master/xcluster/xcluster_universe_replication_alter_helper.h" + +#include "yb/common/xcluster_util.h" +#include "yb/master/catalog_manager-internal.h" +#include "yb/master/catalog_manager.h" +#include "yb/master/master_util.h" +#include "yb/master/xcluster/xcluster_manager.h" +#include "yb/master/xcluster/xcluster_replication_group.h" + +namespace yb::master { + +AlterUniverseReplicationHelper::AlterUniverseReplicationHelper( + Master& master, CatalogManager& catalog_manager, const LeaderEpoch& epoch) + : master_(master), + catalog_manager_(catalog_manager), + sys_catalog_(*catalog_manager.sys_catalog()), + xcluster_manager_(*catalog_manager.GetXClusterManagerImpl()), + epoch_(epoch) {} + +AlterUniverseReplicationHelper::~AlterUniverseReplicationHelper() {} + +Status AlterUniverseReplicationHelper::Alter( + Master& master, CatalogManager& catalog_manager, const AlterUniverseReplicationRequestPB* req, + AlterUniverseReplicationResponsePB* resp, const LeaderEpoch& epoch) { + AlterUniverseReplicationHelper helper(master, catalog_manager, epoch); + return helper.AlterUniverseReplication(req, resp); +} + +Status AlterUniverseReplicationHelper::AlterUniverseReplication( + const AlterUniverseReplicationRequestPB* req, AlterUniverseReplicationResponsePB* resp) { + auto replication_group_id = xcluster::ReplicationGroupId(req->replication_group_id()); + auto original_ri = catalog_manager_.GetUniverseReplication(replication_group_id); + SCHECK_EC_FORMAT( + original_ri, NotFound, MasterError(MasterErrorPB::OBJECT_NOT_FOUND), + "Could not find xCluster replication group $0", replication_group_id); + + // Currently, config options are mutually exclusive to simplify transactionality. + int config_count = (req->producer_master_addresses_size() > 0 ? 1 : 0) + + (req->producer_table_ids_to_remove_size() > 0 ? 1 : 0) + + (req->producer_table_ids_to_add_size() > 0 ? 1 : 0) + + (req->has_new_replication_group_id() ? 1 : 0) + + (!req->producer_namespace_id_to_remove().empty() ? 1 : 0); + SCHECK_EC_FORMAT( + config_count == 1, InvalidArgument, MasterError(MasterErrorPB::INVALID_REQUEST), + "Only 1 Alter operation per request currently supported: $0", req->ShortDebugString()); + + if (req->producer_master_addresses_size() > 0) { + return UpdateProducerAddress(original_ri, req); + } + + if (req->has_producer_namespace_id_to_remove()) { + return RemoveNamespaceFromReplicationGroup( + original_ri, req->producer_namespace_id_to_remove(), catalog_manager_, epoch_); + } + + if (req->producer_table_ids_to_remove_size() > 0) { + std::vector table_ids( + req->producer_table_ids_to_remove().begin(), req->producer_table_ids_to_remove().end()); + return master::RemoveTablesFromReplicationGroup( + original_ri, table_ids, catalog_manager_, epoch_); + } + + if (req->producer_table_ids_to_add_size() > 0) { + RETURN_NOT_OK(AddTablesToReplication(original_ri, req, resp)); + xcluster_manager_.CreateXClusterSafeTimeTableAndStartService(); + return Status::OK(); + } + + if (req->has_new_replication_group_id()) { + return RenameUniverseReplication( + original_ri, xcluster::ReplicationGroupId(req->new_replication_group_id())); + } + + return Status::OK(); +} + +Status AlterUniverseReplicationHelper::UpdateProducerAddress( + scoped_refptr universe, const AlterUniverseReplicationRequestPB* req) { + CHECK_GT(req->producer_master_addresses_size(), 0); + + // TODO: Verify the input. Setup an RPC Task, ListTables, ensure same. + + { + // 1a. Persistent Config: Update the Universe Config for Master. + auto l = universe->LockForWrite(); + l.mutable_data()->pb.mutable_producer_master_addresses()->CopyFrom( + req->producer_master_addresses()); + + // 1b. Persistent Config: Update the Consumer Registry (updates TServers) + auto cluster_config = catalog_manager_.ClusterConfig(); + auto cl = cluster_config->LockForWrite(); + auto replication_group_map = + cl.mutable_data()->pb.mutable_consumer_registry()->mutable_producer_map(); + auto it = replication_group_map->find(req->replication_group_id()); + if (it == replication_group_map->end()) { + LOG(WARNING) << "Valid Producer Universe not in Consumer Registry: " + << req->replication_group_id(); + return STATUS( + NotFound, "Could not find xCluster ReplicationGroup", req->ShortDebugString(), + MasterError(MasterErrorPB::OBJECT_NOT_FOUND)); + } + it->second.mutable_master_addrs()->CopyFrom(req->producer_master_addresses()); + cl.mutable_data()->pb.set_version(cl.mutable_data()->pb.version() + 1); + + { + RETURN_NOT_OK(CheckStatus( + sys_catalog_.Upsert(epoch_, universe.get(), cluster_config.get()), + "Updating universe replication info and cluster config in sys-catalog")); + } + l.Commit(); + cl.Commit(); + } + + // 2. Memory Update: Change xcluster_rpc_tasks (Master cache) + { + auto result = universe->GetOrCreateXClusterRpcTasks(req->producer_master_addresses()); + if (!result.ok()) { + return result.status(); + } + } + + return Status::OK(); +} + +Status AlterUniverseReplicationHelper::AddTablesToReplication( + scoped_refptr universe, const AlterUniverseReplicationRequestPB* req, + AlterUniverseReplicationResponsePB* resp) { + SCHECK_GT(req->producer_table_ids_to_add_size(), 0, InvalidArgument, "No tables specified"); + + if (universe->IsDbScoped()) { + // We either add the entire namespace at once, or one table at a time as they get created. + if (req->has_producer_namespace_to_add()) { + SCHECK( + !req->producer_namespace_to_add().id().empty(), InvalidArgument, "Invalid Namespace Id"); + SCHECK( + !req->producer_namespace_to_add().name().empty(), InvalidArgument, + "Invalid Namespace name"); + SCHECK_EQ( + req->producer_namespace_to_add().database_type(), YQLDatabase::YQL_DATABASE_PGSQL, + InvalidArgument, "Invalid Namespace database_type"); + } else { + SCHECK_EQ( + req->producer_table_ids_to_add_size(), 1, InvalidArgument, + "When adding more than table to a DB scoped replication the namespace info must also be " + "provided"); + } + } else { + SCHECK( + !req->has_producer_namespace_to_add(), InvalidArgument, + "Cannot add namespaces to non DB scoped replication"); + } + + xcluster::ReplicationGroupId alter_replication_group_id(xcluster::GetAlterReplicationGroupId( + xcluster::ReplicationGroupId(req->replication_group_id()))); + + // If user passed in bootstrap ids, check that there is a bootstrap id for every table. + SCHECK( + req->producer_bootstrap_ids_to_add_size() == 0 || + req->producer_table_ids_to_add_size() == req->producer_bootstrap_ids_to_add().size(), + InvalidArgument, "Number of bootstrap ids must be equal to number of tables", + req->ShortDebugString()); + + // Verify no 'alter' command running. + auto alter_ri = catalog_manager_.GetUniverseReplication(alter_replication_group_id); + + { + if (alter_ri != nullptr) { + LOG(INFO) << "Found " << alter_replication_group_id << "... Removing"; + if (alter_ri->LockForRead()->is_deleted_or_failed()) { + // Delete previous Alter if it's completed but failed. + master::DeleteUniverseReplicationRequestPB delete_req; + delete_req.set_replication_group_id(alter_ri->id()); + master::DeleteUniverseReplicationResponsePB delete_resp; + Status s = xcluster_manager_.DeleteUniverseReplication( + &delete_req, &delete_resp, /*rpc=*/nullptr, epoch_); + if (!s.ok()) { + if (delete_resp.has_error()) { + resp->mutable_error()->Swap(delete_resp.mutable_error()); + return s; + } + return SetupError(resp->mutable_error(), s); + } + } else { + return STATUS( + InvalidArgument, "Alter for xCluster ReplicationGroup is already running", + req->ShortDebugString(), MasterError(MasterErrorPB::INVALID_REQUEST)); + } + } + } + + // Map each table id to its corresponding bootstrap id. + std::unordered_map table_id_to_bootstrap_id; + if (req->producer_bootstrap_ids_to_add().size() > 0) { + for (int i = 0; i < req->producer_table_ids_to_add().size(); i++) { + table_id_to_bootstrap_id[req->producer_table_ids_to_add(i)] = + req->producer_bootstrap_ids_to_add(i); + } + + // Ensure that table ids are unique. We need to do this here even though + // the same check is performed by SetupUniverseReplication because + // duplicate table ids can cause a bootstrap id entry in table_id_to_bootstrap_id + // to be overwritten. + if (table_id_to_bootstrap_id.size() != + implicit_cast(req->producer_table_ids_to_add().size())) { + return STATUS( + InvalidArgument, + "When providing bootstrap ids, " + "the list of tables must be unique", + req->ShortDebugString(), MasterError(MasterErrorPB::INVALID_REQUEST)); + } + } + + // Only add new tables. Ignore tables that are currently being replicated. + auto tid_iter = req->producer_table_ids_to_add(); + std::unordered_set new_tables(tid_iter.begin(), tid_iter.end()); + auto original_universe_l = universe->LockForRead(); + auto& original_universe_pb = original_universe_l->pb; + + for (const auto& table_id : original_universe_pb.tables()) { + new_tables.erase(table_id); + } + if (new_tables.empty()) { + return STATUS( + InvalidArgument, "xCluster ReplicationGroup already contains all requested tables", + req->ShortDebugString(), MasterError(MasterErrorPB::INVALID_REQUEST)); + } + + // 1. create an ALTER table request that mirrors the original 'setup_replication'. + master::SetupUniverseReplicationRequestPB setup_req; + master::SetupUniverseReplicationResponsePB setup_resp; + setup_req.set_replication_group_id(alter_replication_group_id.ToString()); + setup_req.mutable_producer_master_addresses()->CopyFrom( + original_universe_pb.producer_master_addresses()); + setup_req.set_transactional(original_universe_pb.transactional()); + + if (req->has_producer_namespace_to_add()) { + *setup_req.add_producer_namespaces() = req->producer_namespace_to_add(); + } + + for (const auto& table_id : new_tables) { + setup_req.add_producer_table_ids(table_id); + + // Add bootstrap id to request if it exists. + auto bootstrap_id = FindOrNull(table_id_to_bootstrap_id, table_id); + if (bootstrap_id) { + setup_req.add_producer_bootstrap_ids(*bootstrap_id); + } + } + + // 2. run the 'setup_replication' pipeline on the ALTER Table + Status s = + xcluster_manager_.SetupUniverseReplication(&setup_req, &setup_resp, /*rpc=*/nullptr, epoch_); + if (!s.ok()) { + if (setup_resp.has_error()) { + resp->mutable_error()->Swap(setup_resp.mutable_error()); + return s; + } + return SetupError(resp->mutable_error(), s); + } + + return Status::OK(); +} + +Status AlterUniverseReplicationHelper::RenameUniverseReplication( + scoped_refptr universe, + const xcluster::ReplicationGroupId new_replication_group_id) { + const xcluster::ReplicationGroupId old_replication_group_id(universe->id()); + SCHECK_NE( + old_replication_group_id, new_replication_group_id, InvalidArgument, + "Old and new replication ids must be different"); + + SCHECK( + catalog_manager_.GetUniverseReplication(new_replication_group_id) == nullptr, InvalidArgument, + "New replication id is already in use"); + + auto l = universe->LockForWrite(); + + // Since the replication_group_id is used as the key, we need to create a new + // UniverseReplicationInfo. + scoped_refptr new_ri = + new UniverseReplicationInfo(new_replication_group_id); + new_ri->mutable_metadata()->StartMutation(); + SysUniverseReplicationEntryPB* metadata = &new_ri->mutable_metadata()->mutable_dirty()->pb; + metadata->CopyFrom(l->pb); + metadata->set_replication_group_id(new_replication_group_id.ToString()); + + // Also need to update internal maps. + auto cluster_config = catalog_manager_.ClusterConfig(); + auto cl = cluster_config->LockForWrite(); + auto replication_group_map = + cl.mutable_data()->pb.mutable_consumer_registry()->mutable_producer_map(); + (*replication_group_map)[new_replication_group_id.ToString()] = + std::move((*replication_group_map)[old_replication_group_id.ToString()]); + replication_group_map->erase(old_replication_group_id.ToString()); + + RETURN_NOT_OK( + catalog_manager_.ReplaceUniverseReplication(*universe, *new_ri, *cluster_config, epoch_)); + + new_ri->mutable_metadata()->CommitMutation(); + cl.Commit(); + + return Status::OK(); +} + +} // namespace yb::master diff --git a/src/yb/master/xcluster/xcluster_universe_replication_alter_helper.h b/src/yb/master/xcluster/xcluster_universe_replication_alter_helper.h new file mode 100644 index 000000000000..8ac5d76a03ce --- /dev/null +++ b/src/yb/master/xcluster/xcluster_universe_replication_alter_helper.h @@ -0,0 +1,71 @@ +// Copyright (c) YugabyteDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations +// under the License. +// + +#pragma once + +#include "yb/cdc/xcluster_types.h" +#include "yb/master/leader_epoch.h" +#include "yb/master/master_fwd.h" + +namespace yb { + +namespace rpc { +class RpcContext; +} // namespace rpc + +namespace master { + +class UniverseReplicationInfo; + +// Helper class to handle AlterUniverseReplication RPC. +// This object will only live as long as the operation is in progress. +class AlterUniverseReplicationHelper { + public: + ~AlterUniverseReplicationHelper(); + + static Status Alter( + Master& master, CatalogManager& catalog_manager, const AlterUniverseReplicationRequestPB* req, + AlterUniverseReplicationResponsePB* resp, const LeaderEpoch& epoch); + + private: + AlterUniverseReplicationHelper( + Master& master, CatalogManager& catalog_manager, const LeaderEpoch& epoch); + + Status AlterUniverseReplication( + const AlterUniverseReplicationRequestPB* req, AlterUniverseReplicationResponsePB* resp); + + Status UpdateProducerAddress( + scoped_refptr universe, + const AlterUniverseReplicationRequestPB* req); + + Status AddTablesToReplication( + scoped_refptr universe, const AlterUniverseReplicationRequestPB* req, + AlterUniverseReplicationResponsePB* resp); + + // Rename an existing Universe Replication. + Status RenameUniverseReplication( + scoped_refptr universe, + const xcluster::ReplicationGroupId new_replication_group_id); + + Master& master_; + CatalogManager& catalog_manager_; + SysCatalogTable& sys_catalog_; + XClusterManager& xcluster_manager_; + const LeaderEpoch epoch_; + + DISALLOW_COPY_AND_ASSIGN(AlterUniverseReplicationHelper); +}; + +} // namespace master + +} // namespace yb diff --git a/src/yb/master/xcluster/xcluster_universe_replication_setup_helper.cc b/src/yb/master/xcluster/xcluster_universe_replication_setup_helper.cc new file mode 100644 index 000000000000..90b6886168cb --- /dev/null +++ b/src/yb/master/xcluster/xcluster_universe_replication_setup_helper.cc @@ -0,0 +1,1380 @@ +// Copyright (c) YugabyteDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations +// under the License. +// + +#include "yb/master/xcluster/xcluster_universe_replication_setup_helper.h" + +#include "yb/client/table_info.h" +#include "yb/client/table.h" +#include "yb/client/xcluster_client.h" + +#include "yb/common/colocated_util.h" +#include "yb/common/common_net.pb.h" +#include "yb/common/xcluster_util.h" + +#include "yb/gutil/bind.h" + +#include "yb/master/catalog_manager-internal.h" +#include "yb/master/catalog_manager.h" +#include "yb/master/leader_epoch.h" +#include "yb/master/master_ddl.pb.h" +#include "yb/master/master_error.h" +#include "yb/master/master_replication.pb.h" +#include "yb/master/master_util.h" +#include "yb/master/master.h" +#include "yb/master/xcluster_consumer_registry_service.h" +#include "yb/master/xcluster_rpc_tasks.h" +#include "yb/master/xcluster/master_xcluster_util.h" +#include "yb/master/xcluster/xcluster_manager.h" +#include "yb/master/xcluster/xcluster_replication_group.h" + +#include "yb/rpc/rpc_context.h" + +#include "yb/util/flags/auto_flags_util.h" +#include "yb/util/status.h" + +DEFINE_RUNTIME_bool(check_bootstrap_required, false, + "Is it necessary to check whether bootstrap is required for Universe Replication."); + +DEFINE_test_flag(bool, allow_ycql_transactional_xcluster, false, + "Determines if xCluster transactional replication on YCQL tables is allowed."); + +DEFINE_test_flag( + bool, fail_universe_replication_merge, false, + "Causes MergeUniverseReplication to fail with an error."); + +DEFINE_test_flag(bool, exit_unfinished_merging, false, + "Whether to exit part way through the merging universe process."); + +DECLARE_bool(enable_xcluster_auto_flag_validation); + +#define RETURN_ACTION_NOT_OK(expr, action) \ + RETURN_NOT_OK_PREPEND((expr), Format("An error occurred while $0", action)) + +namespace yb::master { + +namespace { + +Status ValidateTableListForDbScopedReplication( + UniverseReplicationInfo& universe, const std::vector& namespace_ids, + const std::set& replicated_tables, const CatalogManager& catalog_manager) { + std::set validated_tables; + + for (const auto& namespace_id : namespace_ids) { + auto table_infos = + VERIFY_RESULT(GetTablesEligibleForXClusterReplication(catalog_manager, namespace_id)); + + std::vector missing_tables; + + for (const auto& table_info : table_infos) { + const auto& table_id = table_info->id(); + if (replicated_tables.contains(table_id)) { + validated_tables.insert(table_id); + } else { + missing_tables.push_back(table_id); + } + } + + SCHECK_FORMAT( + missing_tables.empty(), IllegalState, + "Namespace $0 has additional tables that were not added to xCluster DB Scoped replication " + "group $1: $2", + namespace_id, universe.id(), yb::ToString(missing_tables)); + } + + auto diff = STLSetSymmetricDifference(replicated_tables, validated_tables); + SCHECK_FORMAT( + diff.empty(), IllegalState, + "xCluster DB Scoped replication group $0 contains tables $1 that do not belong to replicated " + "namespaces $2", + universe.id(), yb::ToString(diff), yb::ToString(namespace_ids)); + + return Status::OK(); +} + +// Check if the local AutoFlags config is compatible with the source universe and returns the source +// universe AutoFlags config version if they are compatible. +// If they are not compatible, returns a bad status. +// If the source universe is running an older version which does not support AutoFlags compatiblity +// check, returns an invalid AutoFlags config version. +Result GetAutoFlagConfigVersionIfCompatible( + UniverseReplicationInfo& replication_info, const AutoFlagsConfigPB& local_config) { + const auto& replication_group_id = replication_info.ReplicationGroupId(); + + VLOG_WITH_FUNC(2) << "Validating AutoFlags config for replication group: " << replication_group_id + << " with target config version: " << local_config.config_version(); + + auto validate_result = VERIFY_RESULT(ValidateAutoFlagsConfig(replication_info, local_config)); + + if (!validate_result) { + VLOG_WITH_FUNC(2) + << "Source universe of replication group " << replication_group_id + << " is running a version that does not support the AutoFlags compatibility check yet"; + return kInvalidAutoFlagsConfigVersion; + } + + auto& [is_valid, source_version] = *validate_result; + + SCHECK( + is_valid, IllegalState, + "AutoFlags between the universes are not compatible. Upgrade the target universe to a " + "version higher than or equal to the source universe"); + + return source_version; +} + +} // namespace + +SetupUniverseReplicationHelper::SetupUniverseReplicationHelper( + Master& master, CatalogManager& catalog_manager, const LeaderEpoch& epoch) + : master_(master), + catalog_manager_(catalog_manager), + sys_catalog_(*catalog_manager.sys_catalog()), + xcluster_manager_(*catalog_manager.GetXClusterManagerImpl()), + epoch_(epoch) {} + +SetupUniverseReplicationHelper::~SetupUniverseReplicationHelper() {} + +Status SetupUniverseReplicationHelper::Setup( + Master& master, CatalogManager& catalog_manager, const SetupUniverseReplicationRequestPB* req, + SetupUniverseReplicationResponsePB* resp, const LeaderEpoch& epoch) { + auto helper = scoped_refptr( + new SetupUniverseReplicationHelper(master, catalog_manager, epoch)); + return helper->SetupUniverseReplication(req, resp); +} + +void SetupUniverseReplicationHelper::MarkUniverseReplicationFailed( + scoped_refptr universe, const Status& failure_status) { + auto l = universe->LockForWrite(); + MarkUniverseReplicationFailed(failure_status, &l, universe); +} + +void SetupUniverseReplicationHelper::MarkUniverseReplicationFailed( + const Status& failure_status, CowWriteLock* universe_lock, + scoped_refptr universe) { + auto& l = *universe_lock; + if (l->pb.state() == SysUniverseReplicationEntryPB::DELETED) { + l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::DELETED_ERROR); + } else { + l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::FAILED); + } + + LOG(WARNING) << "Universe replication " << universe->ToString() + << " failed: " << failure_status.ToString(); + + universe->SetSetupUniverseReplicationErrorStatus(failure_status); + + // Update sys_catalog. + const Status s = sys_catalog_.Upsert(epoch_, universe); + + l.CommitOrWarn(s, "updating universe replication info in sys-catalog"); +} + +/* + * UniverseReplication is setup in 4 stages within the Catalog Manager + * 1. SetupUniverseReplication: Validates user input & requests Producer schema. + * 2. GetTableSchemaCallback: Validates Schema compatibility & requests Producer xCluster init. + * 3. AddStreamToUniverseAndInitConsumer: Setup RPC connections for xCluster Streaming + * 4. InitXClusterConsumer: Initializes the Consumer settings to begin tailing data + */ +Status SetupUniverseReplicationHelper::SetupUniverseReplication( + const SetupUniverseReplicationRequestPB* req, SetupUniverseReplicationResponsePB* resp) { + // Sanity checking section. + if (!req->has_replication_group_id()) { + return STATUS( + InvalidArgument, "Producer universe ID must be provided", req->ShortDebugString(), + MasterError(MasterErrorPB::INVALID_REQUEST)); + } + + if (req->producer_master_addresses_size() <= 0) { + return STATUS( + InvalidArgument, "Producer master address must be provided", req->ShortDebugString(), + MasterError(MasterErrorPB::INVALID_REQUEST)); + } + + if (req->producer_bootstrap_ids().size() > 0 && + req->producer_bootstrap_ids().size() != req->producer_table_ids().size()) { + return STATUS( + InvalidArgument, "Number of bootstrap ids must be equal to number of tables", + req->ShortDebugString(), MasterError(MasterErrorPB::INVALID_REQUEST)); + } + + { + auto l = catalog_manager_.ClusterConfig()->LockForRead(); + if (l->pb.cluster_uuid() == req->replication_group_id()) { + return STATUS( + InvalidArgument, "The request UUID and cluster UUID are identical.", + req->ShortDebugString(), MasterError(MasterErrorPB::INVALID_REQUEST)); + } + } + + RETURN_NOT_OK_PREPEND( + ValidateMasterAddressesBelongToDifferentCluster(master_, req->producer_master_addresses()), + req->ShortDebugString()); + + SetupReplicationInfo setup_info; + setup_info.transactional = req->transactional(); + auto& table_id_to_bootstrap_id = setup_info.table_bootstrap_ids; + + if (!req->producer_bootstrap_ids().empty()) { + if (req->producer_table_ids().size() != req->producer_bootstrap_ids_size()) { + return STATUS( + InvalidArgument, "Bootstrap ids must be provided for all tables", req->ShortDebugString(), + MasterError(MasterErrorPB::INVALID_REQUEST)); + } + + table_id_to_bootstrap_id.reserve(req->producer_table_ids().size()); + for (int i = 0; i < req->producer_table_ids().size(); i++) { + table_id_to_bootstrap_id.insert_or_assign( + req->producer_table_ids(i), + VERIFY_RESULT(xrepl::StreamId::FromString(req->producer_bootstrap_ids(i)))); + } + } + + SCHECK( + req->producer_namespaces().empty() || req->transactional(), InvalidArgument, + "Transactional flag must be set for Db scoped replication groups"); + + std::vector producer_namespace_ids, consumer_namespace_ids; + for (const auto& producer_ns_id : req->producer_namespaces()) { + SCHECK(!producer_ns_id.id().empty(), InvalidArgument, "Invalid Namespace Id"); + SCHECK(!producer_ns_id.name().empty(), InvalidArgument, "Invalid Namespace name"); + SCHECK_EQ( + producer_ns_id.database_type(), YQLDatabase::YQL_DATABASE_PGSQL, InvalidArgument, + "Invalid Namespace database_type"); + + producer_namespace_ids.push_back(producer_ns_id.id()); + + NamespaceIdentifierPB consumer_ns_id; + consumer_ns_id.set_database_type(YQLDatabase::YQL_DATABASE_PGSQL); + consumer_ns_id.set_name(producer_ns_id.name()); + auto ns_info = VERIFY_RESULT(catalog_manager_.FindNamespace(consumer_ns_id)); + consumer_namespace_ids.push_back(ns_info->id()); + } + + // We should set the universe uuid even if we fail with AlreadyPresent error. + { + auto universe_uuid = catalog_manager_.GetUniverseUuidIfExists(); + if (universe_uuid) { + resp->set_universe_uuid(universe_uuid->ToString()); + } + } + + auto ri = VERIFY_RESULT(CreateUniverseReplicationInfo( + xcluster::ReplicationGroupId(req->replication_group_id()), req->producer_master_addresses(), + producer_namespace_ids, consumer_namespace_ids, req->producer_table_ids(), + setup_info.transactional)); + + // Initialize the xCluster Stream by querying the Producer server for RPC sanity checks. + auto result = ri->GetOrCreateXClusterRpcTasks(req->producer_master_addresses()); + if (!result.ok()) { + MarkUniverseReplicationFailed(ri, ResultToStatus(result)); + return SetupError(resp->mutable_error(), MasterErrorPB::INVALID_REQUEST, result.status()); + } + std::shared_ptr xcluster_rpc = *result; + + // For each table, run an async RPC task to verify a sufficient Producer:Consumer schema match. + for (int i = 0; i < req->producer_table_ids_size(); i++) { + scoped_refptr shared_this = this; + + // SETUP CONTINUES after this async call. + Status s; + if (IsColocatedDbParentTableId(req->producer_table_ids(i))) { + auto tables_info = std::make_shared>(); + s = xcluster_rpc->client()->GetColocatedTabletSchemaByParentTableId( + req->producer_table_ids(i), tables_info, + Bind( + &SetupUniverseReplicationHelper::GetColocatedTabletSchemaCallback, shared_this, + ri->ReplicationGroupId(), tables_info, setup_info)); + } else if (IsTablegroupParentTableId(req->producer_table_ids(i))) { + auto tablegroup_id = GetTablegroupIdFromParentTableId(req->producer_table_ids(i)); + auto tables_info = std::make_shared>(); + s = xcluster_rpc->client()->GetTablegroupSchemaById( + tablegroup_id, tables_info, + Bind( + &SetupUniverseReplicationHelper::GetTablegroupSchemaCallback, shared_this, + ri->ReplicationGroupId(), tables_info, tablegroup_id, setup_info)); + } else { + auto table_info = std::make_shared(); + s = xcluster_rpc->client()->GetTableSchemaById( + req->producer_table_ids(i), table_info, + Bind( + &SetupUniverseReplicationHelper::GetTableSchemaCallback, shared_this, + ri->ReplicationGroupId(), table_info, setup_info)); + } + + if (!s.ok()) { + MarkUniverseReplicationFailed(ri, s); + return SetupError(resp->mutable_error(), MasterErrorPB::INVALID_REQUEST, s); + } + } + + LOG(INFO) << "Started schema validation for universe replication " << ri->ToString(); + return Status::OK(); +} + +Status SetupUniverseReplicationHelper::ValidateMasterAddressesBelongToDifferentCluster( + Master& master, const google::protobuf::RepeatedPtrField& master_addresses) { + std::vector cluster_master_addresses; + RETURN_NOT_OK(master.ListMasters(&cluster_master_addresses)); + std::unordered_set cluster_master_hps; + + for (const auto& cluster_elem : cluster_master_addresses) { + if (cluster_elem.has_registration()) { + auto p_rpc_addresses = cluster_elem.registration().private_rpc_addresses(); + for (const auto& p_rpc_elem : p_rpc_addresses) { + cluster_master_hps.insert(HostPort::FromPB(p_rpc_elem)); + } + + auto broadcast_addresses = cluster_elem.registration().broadcast_addresses(); + for (const auto& bc_elem : broadcast_addresses) { + cluster_master_hps.insert(HostPort::FromPB(bc_elem)); + } + } + + for (const auto& master_address : master_addresses) { + auto master_hp = HostPort::FromPB(master_address); + SCHECK( + !cluster_master_hps.contains(master_hp), InvalidArgument, + "Master address $0 belongs to the target universe", master_hp); + } + } + return Status::OK(); +} + +void SetupUniverseReplicationHelper::GetTableSchemaCallback( + const xcluster::ReplicationGroupId& replication_group_id, + const std::shared_ptr& producer_info, + const SetupReplicationInfo& setup_info, const Status& s) { + // First get the universe. + auto universe = catalog_manager_.GetUniverseReplication(replication_group_id); + if (universe == nullptr) { + LOG(ERROR) << "Universe not found: " << replication_group_id; + return; + } + + std::string action = "getting schema for table"; + auto status = s; + if (status.ok()) { + action = "validating table schema and creating xCluster stream"; + status = ValidateTableAndCreateStreams(universe, producer_info, setup_info); + } + + if (!status.ok()) { + LOG(ERROR) << "Error " << action << ". Universe: " << replication_group_id + << ", Table: " << producer_info->table_id << ": " << status; + MarkUniverseReplicationFailed(universe, status); + } +} + +void SetupUniverseReplicationHelper::GetTablegroupSchemaCallback( + const xcluster::ReplicationGroupId& replication_group_id, + const std::shared_ptr>& infos, + const TablegroupId& producer_tablegroup_id, const SetupReplicationInfo& setup_info, + const Status& s) { + // First get the universe. + auto universe = catalog_manager_.GetUniverseReplication(replication_group_id); + if (universe == nullptr) { + LOG(ERROR) << "Universe not found: " << replication_group_id; + return; + } + + auto status = + GetTablegroupSchemaCallbackInternal(universe, *infos, producer_tablegroup_id, setup_info, s); + if (!status.ok()) { + std::ostringstream oss; + for (size_t i = 0; i < infos->size(); ++i) { + oss << ((i == 0) ? "" : ", ") << (*infos)[i].table_id; + } + LOG(ERROR) << "Error processing for tables: [ " << oss.str() + << " ] for xCluster replication group " << replication_group_id << ": " << status; + MarkUniverseReplicationFailed(universe, status); + } +} + +Status SetupUniverseReplicationHelper::GetTablegroupSchemaCallbackInternal( + scoped_refptr& universe, const std::vector& infos, + const TablegroupId& producer_tablegroup_id, const SetupReplicationInfo& setup_info, + const Status& s) { + RETURN_NOT_OK(s); + + SCHECK(!infos.empty(), IllegalState, Format("Tablegroup $0 is empty", producer_tablegroup_id)); + + // validated_consumer_tables contains the table IDs corresponding to that + // from the producer tables. + std::unordered_set validated_consumer_tables; + ColocationSchemaVersions colocated_schema_versions; + colocated_schema_versions.reserve(infos.size()); + for (const auto& info : infos) { + // Validate each of the member table in the tablegroup. + GetTableSchemaResponsePB resp; + RETURN_NOT_OK(ValidateTableSchemaForXCluster(info, setup_info, &resp)); + + colocated_schema_versions.emplace_back( + resp.schema().colocated_table_id().colocation_id(), info.schema.version(), resp.version()); + validated_consumer_tables.insert(resp.identifier().table_id()); + } + + // Get the consumer tablegroup ID. Since this call is expensive (one needs to reverse lookup + // the tablegroup ID from table ID), we only do this call once and do validation afterward. + TablegroupId consumer_tablegroup_id; + // Starting Colocation GA, colocated databases create implicit underlying tablegroups. + bool colocated_database; + RETURN_NOT_OK(catalog_manager_.GetTableGroupAndColocationInfo( + *validated_consumer_tables.begin(), consumer_tablegroup_id, colocated_database)); + + // tables_in_consumer_tablegroup are the tables listed within the consumer_tablegroup_id. + // We need validated_consumer_tables and tables_in_consumer_tablegroup to be identical. + std::unordered_set tables_in_consumer_tablegroup; + { + GetTablegroupSchemaRequestPB req; + GetTablegroupSchemaResponsePB resp; + req.mutable_tablegroup()->set_id(consumer_tablegroup_id); + auto status = catalog_manager_.GetTablegroupSchema(&req, &resp); + if (status.ok() && resp.has_error()) { + status = StatusFromPB(resp.error().status()); + } + RETURN_NOT_OK_PREPEND( + status, + Format("Error when getting consumer tablegroup schema: $0", consumer_tablegroup_id)); + + for (const auto& info : resp.get_table_schema_response_pbs()) { + tables_in_consumer_tablegroup.insert(info.identifier().table_id()); + } + } + + if (validated_consumer_tables != tables_in_consumer_tablegroup) { + return STATUS( + IllegalState, + Format( + "Mismatch between tables associated with producer tablegroup $0 and " + "tables in consumer tablegroup $1: ($2) vs ($3).", + producer_tablegroup_id, consumer_tablegroup_id, AsString(validated_consumer_tables), + AsString(tables_in_consumer_tablegroup))); + } + + RETURN_NOT_OK_PREPEND( + IsBootstrapRequiredOnProducer( + universe, producer_tablegroup_id, setup_info.table_bootstrap_ids), + Format( + "Found error while checking if bootstrap is required for table $0", + producer_tablegroup_id)); + + TableId producer_parent_table_id; + TableId consumer_parent_table_id; + if (colocated_database) { + producer_parent_table_id = GetColocationParentTableId(producer_tablegroup_id); + consumer_parent_table_id = GetColocationParentTableId(consumer_tablegroup_id); + } else { + producer_parent_table_id = GetTablegroupParentTableId(producer_tablegroup_id); + consumer_parent_table_id = GetTablegroupParentTableId(consumer_tablegroup_id); + } + + SCHECK( + !xcluster_manager_.IsTableReplicationConsumer(consumer_parent_table_id), IllegalState, + "N:1 replication topology not supported"); + + RETURN_NOT_OK(AddValidatedTableAndCreateStreams( + universe, setup_info.table_bootstrap_ids, producer_parent_table_id, consumer_parent_table_id, + colocated_schema_versions)); + return Status::OK(); +} + +void SetupUniverseReplicationHelper::GetColocatedTabletSchemaCallback( + const xcluster::ReplicationGroupId& replication_group_id, + const std::shared_ptr>& infos, + const SetupReplicationInfo& setup_info, const Status& s) { + // First get the universe. + auto universe = catalog_manager_.GetUniverseReplication(replication_group_id); + if (universe == nullptr) { + LOG(ERROR) << "Universe not found: " << replication_group_id; + return; + } + + if (!s.ok()) { + MarkUniverseReplicationFailed(universe, s); + std::ostringstream oss; + for (size_t i = 0; i < infos->size(); ++i) { + oss << ((i == 0) ? "" : ", ") << (*infos)[i].table_id; + } + LOG(ERROR) << "Error getting schema for tables: [ " << oss.str() << " ]: " << s; + return; + } + + if (infos->empty()) { + LOG(WARNING) << "Received empty list of tables to validate: " << s; + return; + } + + // Validate table schemas. + std::unordered_set producer_parent_table_ids; + std::unordered_set consumer_parent_table_ids; + ColocationSchemaVersions colocated_schema_versions; + colocated_schema_versions.reserve(infos->size()); + for (const auto& info : *infos) { + // Verify that we have a colocated table. + if (!info.colocated) { + MarkUniverseReplicationFailed( + universe, + STATUS(InvalidArgument, Format("Received non-colocated table: $0", info.table_id))); + LOG(ERROR) << "Received non-colocated table: " << info.table_id; + return; + } + // Validate each table, and get the parent colocated table id for the consumer. + GetTableSchemaResponsePB resp; + Status table_status = ValidateTableSchemaForXCluster(info, setup_info, &resp); + if (!table_status.ok()) { + MarkUniverseReplicationFailed(universe, table_status); + LOG(ERROR) << "Found error while validating table schema for table " << info.table_id << ": " + << table_status; + return; + } + // Store the parent table ids. + producer_parent_table_ids.insert(GetColocatedDbParentTableId(info.table_name.namespace_id())); + consumer_parent_table_ids.insert( + GetColocatedDbParentTableId(resp.identifier().namespace_().id())); + colocated_schema_versions.emplace_back( + resp.schema().colocated_table_id().colocation_id(), info.schema.version(), resp.version()); + } + + // Verify that we only found one producer and one consumer colocated parent table id. + if (producer_parent_table_ids.size() != 1) { + auto message = Format( + "Found incorrect number of producer colocated parent table ids. " + "Expected 1, but found: $0", + AsString(producer_parent_table_ids)); + MarkUniverseReplicationFailed(universe, STATUS(InvalidArgument, message)); + LOG(ERROR) << message; + return; + } + if (consumer_parent_table_ids.size() != 1) { + auto message = Format( + "Found incorrect number of consumer colocated parent table ids. " + "Expected 1, but found: $0", + AsString(consumer_parent_table_ids)); + MarkUniverseReplicationFailed(universe, STATUS(InvalidArgument, message)); + LOG(ERROR) << message; + return; + } + + if (xcluster_manager_.IsTableReplicationConsumer(*consumer_parent_table_ids.begin())) { + std::string message = "N:1 replication topology not supported"; + MarkUniverseReplicationFailed(universe, STATUS(IllegalState, message)); + LOG(ERROR) << message; + return; + } + + Status status = IsBootstrapRequiredOnProducer( + universe, *producer_parent_table_ids.begin(), setup_info.table_bootstrap_ids); + if (!status.ok()) { + MarkUniverseReplicationFailed(universe, status); + LOG(ERROR) << "Found error while checking if bootstrap is required for table " + << *producer_parent_table_ids.begin() << ": " << status; + } + + status = AddValidatedTableAndCreateStreams( + universe, setup_info.table_bootstrap_ids, *producer_parent_table_ids.begin(), + *consumer_parent_table_ids.begin(), colocated_schema_versions); + + if (!status.ok()) { + LOG(ERROR) << "Found error while adding validated table to system catalog: " + << *producer_parent_table_ids.begin() << ": " << status; + return; + } +} + +Status SetupUniverseReplicationHelper::ValidateTableAndCreateStreams( + scoped_refptr universe, + const std::shared_ptr& producer_info, + const SetupReplicationInfo& setup_info) { + auto l = universe->LockForWrite(); + if (producer_info->table_name.namespace_name() == master::kSystemNamespaceName) { + auto status = STATUS(IllegalState, "Cannot replicate system tables."); + MarkUniverseReplicationFailed(status, &l, universe); + return status; + } + RETURN_ACTION_NOT_OK( + sys_catalog_.Upsert(epoch_, universe), "updating system tables in universe replication"); + l.Commit(); + + GetTableSchemaResponsePB consumer_schema; + RETURN_NOT_OK(ValidateTableSchemaForXCluster(*producer_info, setup_info, &consumer_schema)); + + // If Bootstrap Id is passed in then it must be provided for all tables. + const auto& producer_bootstrap_ids = setup_info.table_bootstrap_ids; + SCHECK( + producer_bootstrap_ids.empty() || producer_bootstrap_ids.contains(producer_info->table_id), + NotFound, + Format("Bootstrap id not found for table $0", producer_info->table_name.ToString())); + + RETURN_NOT_OK( + IsBootstrapRequiredOnProducer(universe, producer_info->table_id, producer_bootstrap_ids)); + + SchemaVersion producer_schema_version = producer_info->schema.version(); + SchemaVersion consumer_schema_version = consumer_schema.version(); + ColocationSchemaVersions colocated_schema_versions; + RETURN_NOT_OK(AddValidatedTableToUniverseReplication( + universe, producer_info->table_id, consumer_schema.identifier().table_id(), + producer_schema_version, consumer_schema_version, colocated_schema_versions)); + + return CreateStreamsIfReplicationValidated(universe, producer_bootstrap_ids); +} + +Status SetupUniverseReplicationHelper::ValidateTableSchemaForXCluster( + const client::YBTableInfo& info, const SetupReplicationInfo& setup_info, + GetTableSchemaResponsePB* resp) { + bool is_ysql_table = info.table_type == client::YBTableType::PGSQL_TABLE_TYPE; + if (setup_info.transactional && !GetAtomicFlag(&FLAGS_TEST_allow_ycql_transactional_xcluster) && + !is_ysql_table) { + return STATUS_FORMAT( + NotSupported, "Transactional replication is not supported for non-YSQL tables: $0", + info.table_name.ToString()); + } + + // Get corresponding table schema on local universe. + GetTableSchemaRequestPB req; + + auto* table = req.mutable_table(); + table->set_table_name(info.table_name.table_name()); + table->mutable_namespace_()->set_name(info.table_name.namespace_name()); + table->mutable_namespace_()->set_database_type( + GetDatabaseTypeForTable(client::ClientToPBTableType(info.table_type))); + + // Since YSQL tables are not present in table map, we first need to list tables to get the table + // ID and then get table schema. + // Remove this once table maps are fixed for YSQL. + ListTablesRequestPB list_req; + ListTablesResponsePB list_resp; + + list_req.set_name_filter(info.table_name.table_name()); + Status status = catalog_manager_.ListTables(&list_req, &list_resp); + SCHECK( + status.ok() && !list_resp.has_error(), NotFound, + Format("Error while listing table: $0", status.ToString())); + + const auto& source_schema = client::internal::GetSchema(info.schema); + for (const auto& t : list_resp.tables()) { + // Check that table name and namespace both match. + if (t.name() != info.table_name.table_name() || + t.namespace_().name() != info.table_name.namespace_name()) { + continue; + } + + // Check that schema name matches for YSQL tables, if the field is empty, fill in that + // information during GetTableSchema call later. + bool has_valid_pgschema_name = !t.pgschema_name().empty(); + if (is_ysql_table && has_valid_pgschema_name && + t.pgschema_name() != source_schema.SchemaName()) { + continue; + } + + // Get the table schema. + table->set_table_id(t.id()); + status = catalog_manager_.GetTableSchema(&req, resp); + SCHECK( + status.ok() && !resp->has_error(), NotFound, + Format("Error while getting table schema: $0", status.ToString())); + + // Double-check schema name here if the previous check was skipped. + if (is_ysql_table && !has_valid_pgschema_name) { + std::string target_schema_name = resp->schema().pgschema_name(); + if (target_schema_name != source_schema.SchemaName()) { + table->clear_table_id(); + continue; + } + } + + // Verify that the table on the target side supports replication. + if (is_ysql_table && t.has_relation_type() && t.relation_type() == MATVIEW_TABLE_RELATION) { + return STATUS_FORMAT( + NotSupported, "Replication is not supported for materialized view: $0", + info.table_name.ToString()); + } + + Schema consumer_schema; + auto result = SchemaFromPB(resp->schema(), &consumer_schema); + + // We now have a table match. Validate the schema. + SCHECK( + result.ok() && consumer_schema.EquivalentForDataCopy(source_schema), IllegalState, + Format( + "Source and target schemas don't match: " + "Source: $0, Target: $1, Source schema: $2, Target schema: $3", + info.table_id, resp->identifier().table_id(), info.schema.ToString(), + resp->schema().DebugString())); + break; + } + + SCHECK( + table->has_table_id(), NotFound, + Format( + "Could not find matching table for $0$1", info.table_name.ToString(), + (is_ysql_table ? " pgschema_name: " + source_schema.SchemaName() : ""))); + + // Still need to make map of table id to resp table id (to add to validated map) + // For colocated tables, only add the parent table since we only added the parent table to the + // original pb (we use the number of tables in the pb to determine when validation is done). + if (info.colocated) { + // We require that colocated tables have the same colocation ID. + // + // Backward compatibility: tables created prior to #7378 use YSQL table OID as a colocation ID. + auto source_clc_id = info.schema.has_colocation_id() + ? info.schema.colocation_id() + : CHECK_RESULT(GetPgsqlTableOid(info.table_id)); + auto target_clc_id = (resp->schema().has_colocated_table_id() && + resp->schema().colocated_table_id().has_colocation_id()) + ? resp->schema().colocated_table_id().colocation_id() + : CHECK_RESULT(GetPgsqlTableOid(resp->identifier().table_id())); + SCHECK( + source_clc_id == target_clc_id, IllegalState, + Format( + "Source and target colocation IDs don't match for colocated table: " + "Source: $0, Target: $1, Source colocation ID: $2, Target colocation ID: $3", + info.table_id, resp->identifier().table_id(), source_clc_id, target_clc_id)); + } + + SCHECK( + !xcluster_manager_.IsTableReplicationConsumer(table->table_id()), IllegalState, + "N:1 replication topology not supported"); + + return Status::OK(); +} + +Status SetupUniverseReplicationHelper::IsBootstrapRequiredOnProducer( + scoped_refptr universe, const TableId& producer_table, + const std::unordered_map& table_bootstrap_ids) { + if (!FLAGS_check_bootstrap_required) { + return Status::OK(); + } + auto master_addresses = universe->LockForRead()->pb.producer_master_addresses(); + boost::optional bootstrap_id; + if (table_bootstrap_ids.count(producer_table) > 0) { + bootstrap_id = table_bootstrap_ids.at(producer_table); + } + + auto xcluster_rpc = VERIFY_RESULT(universe->GetOrCreateXClusterRpcTasks(master_addresses)); + if (VERIFY_RESULT(xcluster_rpc->client()->IsBootstrapRequired({producer_table}, bootstrap_id))) { + return STATUS( + IllegalState, + Format( + "Error Missing Data in Logs. Bootstrap is required for producer $0", universe->id())); + } + return Status::OK(); +} + +Status SetupUniverseReplicationHelper::AddValidatedTableToUniverseReplication( + scoped_refptr universe, const TableId& producer_table, + const TableId& consumer_table, const SchemaVersion& producer_schema_version, + const SchemaVersion& consumer_schema_version, + const ColocationSchemaVersions& colocated_schema_versions) { + auto l = universe->LockForWrite(); + + auto map = l.mutable_data()->pb.mutable_validated_tables(); + (*map)[producer_table] = consumer_table; + + SchemaVersionMappingEntryPB entry; + if (IsColocationParentTableId(consumer_table)) { + for (const auto& [colocation_id, producer_schema_version, consumer_schema_version] : + colocated_schema_versions) { + auto colocated_entry = entry.add_colocated_schema_versions(); + auto colocation_mapping = colocated_entry->mutable_schema_version_mapping(); + colocated_entry->set_colocation_id(colocation_id); + colocation_mapping->set_producer_schema_version(producer_schema_version); + colocation_mapping->set_consumer_schema_version(consumer_schema_version); + } + } else { + auto mapping = entry.mutable_schema_version_mapping(); + mapping->set_producer_schema_version(producer_schema_version); + mapping->set_consumer_schema_version(consumer_schema_version); + } + + auto schema_versions_map = l.mutable_data()->pb.mutable_schema_version_mappings(); + (*schema_versions_map)[producer_table] = std::move(entry); + + // TODO: end of config validation should be where SetupUniverseReplication exits back to user + LOG(INFO) << "UpdateItem in AddValidatedTable"; + + // Update sys_catalog. + RETURN_ACTION_NOT_OK( + sys_catalog_.Upsert(epoch_, universe), "updating universe replication info in sys-catalog"); + l.Commit(); + + return Status::OK(); +} + +Status SetupUniverseReplicationHelper::AddValidatedTableAndCreateStreams( + scoped_refptr universe, + const std::unordered_map& table_bootstrap_ids, + const TableId& producer_table, const TableId& consumer_table, + const ColocationSchemaVersions& colocated_schema_versions) { + RETURN_NOT_OK(AddValidatedTableToUniverseReplication( + universe, producer_table, consumer_table, cdc::kInvalidSchemaVersion, + cdc::kInvalidSchemaVersion, colocated_schema_versions)); + return CreateStreamsIfReplicationValidated(universe, table_bootstrap_ids); +} + +Status SetupUniverseReplicationHelper::CreateStreamsIfReplicationValidated( + scoped_refptr universe, + const std::unordered_map& table_bootstrap_ids) { + auto l = universe->LockForWrite(); + if (l->is_deleted_or_failed()) { + // Nothing to do since universe is being deleted. + return STATUS(Aborted, "Universe is being deleted"); + } + + auto* mutable_pb = &l.mutable_data()->pb; + + if (mutable_pb->state() != SysUniverseReplicationEntryPB::INITIALIZING) { + VLOG_WITH_FUNC(2) << "Universe replication is in invalid state " << l->pb.state(); + + // Replication stream has already been validated, or is in FAILED state which cannot be + // recovered. + return Status::OK(); + } + + if (mutable_pb->validated_tables_size() != mutable_pb->tables_size()) { + // Replication stream is not yet ready. All the tables have to be validated. + return Status::OK(); + } + + auto master_addresses = mutable_pb->producer_master_addresses(); + cdc::StreamModeTransactional transactional(mutable_pb->transactional()); + auto res = universe->GetOrCreateXClusterRpcTasks(master_addresses); + if (!res.ok()) { + MarkUniverseReplicationFailed(res.status(), &l, universe); + return STATUS( + InternalError, Format( + "Error while setting up client for producer $0: $1", universe->id(), + res.status().ToString())); + } + std::shared_ptr xcluster_rpc = *res; + + // Now, all tables are validated. + std::vector validated_tables; + auto& tbl_iter = mutable_pb->tables(); + validated_tables.insert(validated_tables.begin(), tbl_iter.begin(), tbl_iter.end()); + + mutable_pb->set_state(SysUniverseReplicationEntryPB::VALIDATED); + // Update sys_catalog. + RETURN_ACTION_NOT_OK( + sys_catalog_.Upsert(epoch_, universe), "updating universe replication info in sys-catalog"); + l.Commit(); + + // Create xCluster stream for each validated table, after persisting the replication state change. + if (!validated_tables.empty()) { + // Keep track of the bootstrap_id, table_id, and options of streams to update after + // the last GetStreamCallback finishes. Will be updated by multiple async + // GetStreamCallback. + auto stream_update_infos = std::make_shared(); + stream_update_infos->reserve(validated_tables.size()); + auto update_infos_lock = std::make_shared(); + + for (const auto& table : validated_tables) { + scoped_refptr shared_this = this; + + auto producer_bootstrap_id = FindOrNull(table_bootstrap_ids, table); + if (producer_bootstrap_id && *producer_bootstrap_id) { + auto table_id = std::make_shared(); + auto stream_options = std::make_shared>(); + xcluster_rpc->client()->GetCDCStream( + *producer_bootstrap_id, table_id, stream_options, + std::bind( + &SetupUniverseReplicationHelper::GetStreamCallback, shared_this, + *producer_bootstrap_id, table_id, stream_options, universe->ReplicationGroupId(), + table, xcluster_rpc, std::placeholders::_1, stream_update_infos, + update_infos_lock)); + } else { + // Streams are used as soon as they are created so set state to active. + client::XClusterClient(*xcluster_rpc->client()) + .CreateXClusterStreamAsync( + table, /*active=*/true, transactional, + std::bind( + &SetupUniverseReplicationHelper::AddStreamToUniverseAndInitConsumer, + shared_this, universe->ReplicationGroupId(), table, std::placeholders::_1, + nullptr /* on_success_cb */)); + } + } + } + return Status::OK(); +} + +void SetupUniverseReplicationHelper::GetStreamCallback( + const xrepl::StreamId& bootstrap_id, std::shared_ptr table_id, + std::shared_ptr> options, + const xcluster::ReplicationGroupId& replication_group_id, const TableId& table, + std::shared_ptr xcluster_rpc, const Status& s, + std::shared_ptr stream_update_infos, + std::shared_ptr update_infos_lock) { + if (!s.ok()) { + LOG(ERROR) << "Unable to find bootstrap id " << bootstrap_id; + AddStreamToUniverseAndInitConsumer(replication_group_id, table, s); + return; + } + + if (*table_id != table) { + const Status invalid_bootstrap_id_status = STATUS_FORMAT( + InvalidArgument, "Invalid bootstrap id for table $0. Bootstrap id $1 belongs to table $2", + table, bootstrap_id, *table_id); + LOG(ERROR) << invalid_bootstrap_id_status; + AddStreamToUniverseAndInitConsumer(replication_group_id, table, invalid_bootstrap_id_status); + return; + } + + auto original_universe = catalog_manager_.GetUniverseReplication(replication_group_id); + + if (original_universe == nullptr) { + LOG(ERROR) << "Universe not found: " << replication_group_id; + return; + } + + cdc::StreamModeTransactional transactional(original_universe->LockForRead()->pb.transactional()); + + // todo check options + { + std::lock_guard lock(*update_infos_lock); + stream_update_infos->push_back({bootstrap_id, *table_id, *options}); + } + + const auto update_xrepl_stream_func = [&]() -> Status { + // Extra callback on universe setup success - update the producer to let it know that + // the bootstrapping is complete. This callback will only be called once among all + // the GetStreamCallback calls, and we update all streams in batch at once. + + std::vector update_bootstrap_ids; + std::vector update_entries; + { + std::lock_guard lock(*update_infos_lock); + + for (const auto& [update_bootstrap_id, update_table_id, update_options] : + *stream_update_infos) { + SysCDCStreamEntryPB new_entry; + new_entry.add_table_id(update_table_id); + new_entry.mutable_options()->Reserve(narrow_cast(update_options.size())); + for (const auto& [key, value] : update_options) { + if (key == cdc::kStreamState) { + // We will set state explicitly. + continue; + } + auto new_option = new_entry.add_options(); + new_option->set_key(key); + new_option->set_value(value); + } + new_entry.set_state(master::SysCDCStreamEntryPB::ACTIVE); + new_entry.set_transactional(transactional); + + update_bootstrap_ids.push_back(update_bootstrap_id); + update_entries.push_back(new_entry); + } + } + + RETURN_NOT_OK_PREPEND( + xcluster_rpc->client()->UpdateCDCStream(update_bootstrap_ids, update_entries), + "Unable to update xrepl stream options on source universe"); + + { + std::lock_guard lock(*update_infos_lock); + stream_update_infos->clear(); + } + return Status::OK(); + }; + + AddStreamToUniverseAndInitConsumer( + replication_group_id, table, bootstrap_id, update_xrepl_stream_func); +} + +void SetupUniverseReplicationHelper::AddStreamToUniverseAndInitConsumer( + const xcluster::ReplicationGroupId& replication_group_id, const TableId& table_id, + const Result& stream_id, std::function on_success_cb) { + auto universe = catalog_manager_.GetUniverseReplication(replication_group_id); + if (universe == nullptr) { + LOG(ERROR) << "Universe not found: " << replication_group_id; + return; + } + + Status s; + if (!stream_id.ok()) { + s = std::move(stream_id).status(); + } else { + s = AddStreamToUniverseAndInitConsumerInternal( + universe, table_id, *stream_id, std::move(on_success_cb)); + } + + if (!s.ok()) { + MarkUniverseReplicationFailed(universe, s); + } +} + +Status SetupUniverseReplicationHelper::AddStreamToUniverseAndInitConsumerInternal( + scoped_refptr universe, const TableId& table_id, + const xrepl::StreamId& stream_id, std::function on_success_cb) { + bool merge_alter = false; + bool validated_all_tables = false; + std::vector consumer_info; + { + auto l = universe->LockForWrite(); + if (l->is_deleted_or_failed()) { + // Nothing to do if universe is being deleted. + return Status::OK(); + } + + auto map = l.mutable_data()->pb.mutable_table_streams(); + (*map)[table_id] = stream_id.ToString(); + + // This functions as a barrier: waiting for the last RPC call from GetTableSchemaCallback. + if (l.mutable_data()->pb.table_streams_size() == l->pb.tables_size()) { + // All tables successfully validated! Register xCluster consumers & start replication. + validated_all_tables = true; + LOG(INFO) << "Registering xCluster consumers for universe " << universe->id(); + + consumer_info.reserve(l->pb.tables_size()); + std::set consumer_table_ids; + for (const auto& [producer_table_id, consumer_table_id] : l->pb.validated_tables()) { + consumer_table_ids.insert(consumer_table_id); + + XClusterConsumerStreamInfo info; + info.producer_table_id = producer_table_id; + info.consumer_table_id = consumer_table_id; + info.stream_id = VERIFY_RESULT(xrepl::StreamId::FromString((*map)[producer_table_id])); + consumer_info.push_back(info); + } + + if (l->IsDbScoped()) { + std::vector consumer_namespace_ids; + for (const auto& ns_info : l->pb.db_scoped_info().namespace_infos()) { + consumer_namespace_ids.push_back(ns_info.consumer_namespace_id()); + } + RETURN_NOT_OK(ValidateTableListForDbScopedReplication( + *universe, consumer_namespace_ids, consumer_table_ids, catalog_manager_)); + } + + std::vector hp; + HostPortsFromPBs(l->pb.producer_master_addresses(), &hp); + auto xcluster_rpc_tasks = + VERIFY_RESULT(universe->GetOrCreateXClusterRpcTasks(l->pb.producer_master_addresses())); + RETURN_NOT_OK(InitXClusterConsumer( + consumer_info, HostPort::ToCommaSeparatedString(hp), *universe.get(), + xcluster_rpc_tasks)); + + if (xcluster::IsAlterReplicationGroupId(universe->ReplicationGroupId())) { + // Don't enable ALTER universes, merge them into the main universe instead. + // on_success_cb will be invoked in MergeUniverseReplication. + merge_alter = true; + } else { + l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::ACTIVE); + if (on_success_cb) { + // Before updating, run any callbacks on success. + RETURN_NOT_OK(on_success_cb()); + } + } + } + + // Update sys_catalog with new producer table id info. + RETURN_NOT_OK(sys_catalog_.Upsert(epoch_, universe)); + + l.Commit(); + } + + if (!validated_all_tables) { + return Status::OK(); + } + + auto final_id = xcluster::GetOriginalReplicationGroupId(universe->ReplicationGroupId()); + // If this is an 'alter', merge back into primary command now that setup is a success. + if (merge_alter) { + RETURN_NOT_OK(MergeUniverseReplication(universe, final_id, std::move(on_success_cb))); + } + // Update the in-memory cache of consumer tables. + for (const auto& info : consumer_info) { + xcluster_manager_.RecordTableConsumerStream(info.consumer_table_id, final_id, info.stream_id); + } + + return Status::OK(); +} + +Status SetupUniverseReplicationHelper::InitXClusterConsumer( + const std::vector& consumer_info, const std::string& master_addrs, + UniverseReplicationInfo& replication_info, + std::shared_ptr xcluster_rpc_tasks) { + auto universe_l = replication_info.LockForRead(); + auto schema_version_mappings = universe_l->pb.schema_version_mappings(); + + // Get the tablets in the consumer table. + cdc::ProducerEntryPB producer_entry; + + if (FLAGS_enable_xcluster_auto_flag_validation) { + auto compatible_auto_flag_config_version = VERIFY_RESULT( + GetAutoFlagConfigVersionIfCompatible(replication_info, master_.GetAutoFlagsConfig())); + producer_entry.set_compatible_auto_flag_config_version(compatible_auto_flag_config_version); + producer_entry.set_validated_auto_flags_config_version(compatible_auto_flag_config_version); + } + + auto cluster_config = catalog_manager_.ClusterConfig(); + auto l = cluster_config->LockForWrite(); + auto* consumer_registry = l.mutable_data()->pb.mutable_consumer_registry(); + auto transactional = universe_l->pb.transactional(); + if (!xcluster::IsAlterReplicationGroupId(replication_info.ReplicationGroupId())) { + if (universe_l->IsDbScoped()) { + DCHECK(transactional); + } + } + + for (const auto& stream_info : consumer_info) { + auto consumer_tablet_keys = + VERIFY_RESULT(catalog_manager_.GetTableKeyRanges(stream_info.consumer_table_id)); + auto schema_version = + VERIFY_RESULT(catalog_manager_.GetTableSchemaVersion(stream_info.consumer_table_id)); + + cdc::StreamEntryPB stream_entry; + // Get producer tablets and map them to the consumer tablets + RETURN_NOT_OK(InitXClusterStream( + stream_info.producer_table_id, stream_info.consumer_table_id, consumer_tablet_keys, + &stream_entry, xcluster_rpc_tasks)); + // Set the validated consumer schema version + auto* producer_schema_pb = stream_entry.mutable_producer_schema(); + producer_schema_pb->set_last_compatible_consumer_schema_version(schema_version); + auto* schema_versions = stream_entry.mutable_schema_versions(); + auto mapping = FindOrNull(schema_version_mappings, stream_info.producer_table_id); + SCHECK(mapping, NotFound, Format("No schema mapping for $0", stream_info.producer_table_id)); + if (IsColocationParentTableId(stream_info.consumer_table_id)) { + // Get all the child tables and add their mappings + auto& colocated_schema_versions_pb = *stream_entry.mutable_colocated_schema_versions(); + for (const auto& colocated_entry : mapping->colocated_schema_versions()) { + auto colocation_id = colocated_entry.colocation_id(); + colocated_schema_versions_pb[colocation_id].set_current_producer_schema_version( + colocated_entry.schema_version_mapping().producer_schema_version()); + colocated_schema_versions_pb[colocation_id].set_current_consumer_schema_version( + colocated_entry.schema_version_mapping().consumer_schema_version()); + } + } else { + schema_versions->set_current_producer_schema_version( + mapping->schema_version_mapping().producer_schema_version()); + schema_versions->set_current_consumer_schema_version( + mapping->schema_version_mapping().consumer_schema_version()); + } + + // Mark this stream as special if it is for the ddl_queue table. + auto table_info = catalog_manager_.GetTableInfo(stream_info.consumer_table_id); + stream_entry.set_is_ddl_queue_table( + table_info->GetTableType() == PGSQL_TABLE_TYPE && + table_info->name() == xcluster::kDDLQueueTableName && + table_info->pgschema_name() == xcluster::kDDLQueuePgSchemaName); + + (*producer_entry.mutable_stream_map())[stream_info.stream_id.ToString()] = + std::move(stream_entry); + } + + // Log the Network topology of the Producer Cluster + auto master_addrs_list = StringSplit(master_addrs, ','); + producer_entry.mutable_master_addrs()->Reserve(narrow_cast(master_addrs_list.size())); + for (const auto& addr : master_addrs_list) { + auto hp = VERIFY_RESULT(HostPort::FromString(addr, 0)); + HostPortToPB(hp, producer_entry.add_master_addrs()); + } + + auto* replication_group_map = consumer_registry->mutable_producer_map(); + SCHECK_EQ( + replication_group_map->count(replication_info.id()), 0, InvalidArgument, + "Already created a consumer for this universe"); + + // TServers will use the ClusterConfig to create xCluster Consumers for applicable local tablets. + (*replication_group_map)[replication_info.id()] = std::move(producer_entry); + + l.mutable_data()->pb.set_version(l.mutable_data()->pb.version() + 1); + RETURN_NOT_OK(CheckStatus( + sys_catalog_.Upsert(epoch_, cluster_config.get()), "updating cluster config in sys-catalog")); + + xcluster_manager_.SyncConsumerReplicationStatusMap( + replication_info.ReplicationGroupId(), *replication_group_map); + l.Commit(); + + xcluster_manager_.CreateXClusterSafeTimeTableAndStartService(); + + return Status::OK(); +} + +Status SetupUniverseReplicationHelper::MergeUniverseReplication( + scoped_refptr universe, xcluster::ReplicationGroupId original_id, + std::function on_success_cb) { + // Merge back into primary command now that setup is a success. + LOG(INFO) << "Merging xCluster ReplicationGroup: " << universe->id() << " into " << original_id; + + SCHECK( + !FLAGS_TEST_fail_universe_replication_merge, IllegalState, + "TEST_fail_universe_replication_merge"); + + auto original_universe = catalog_manager_.GetUniverseReplication(original_id); + if (original_universe == nullptr) { + LOG(ERROR) << "Universe not found: " << original_id; + return Status::OK(); + } + + { + auto cluster_config = catalog_manager_.ClusterConfig(); + // Acquire Locks in order of Original Universe, Cluster Config, New Universe + auto original_lock = original_universe->LockForWrite(); + auto alter_lock = universe->LockForWrite(); + auto cl = cluster_config->LockForWrite(); + + // Merge Cluster Config for TServers. + auto* consumer_registry = cl.mutable_data()->pb.mutable_consumer_registry(); + auto pm = consumer_registry->mutable_producer_map(); + auto original_producer_entry = pm->find(original_universe->id()); + auto alter_producer_entry = pm->find(universe->id()); + if (original_producer_entry != pm->end() && alter_producer_entry != pm->end()) { + // Merge the Tables from the Alter into the original. + auto as = alter_producer_entry->second.stream_map(); + original_producer_entry->second.mutable_stream_map()->insert(as.begin(), as.end()); + // Delete the Alter + pm->erase(alter_producer_entry); + } else { + LOG(WARNING) << "Could not find both universes in Cluster Config: " << universe->id(); + } + cl.mutable_data()->pb.set_version(cl.mutable_data()->pb.version() + 1); + + // Merge Master Config on Consumer. (no need for Producer changes, since it uses stream_id) + // Merge Table->StreamID mapping. + auto& alter_pb = alter_lock.mutable_data()->pb; + auto& original_pb = original_lock.mutable_data()->pb; + + auto* alter_tables = alter_pb.mutable_tables(); + original_pb.mutable_tables()->MergeFrom(*alter_tables); + alter_tables->Clear(); + auto* alter_table_streams = alter_pb.mutable_table_streams(); + original_pb.mutable_table_streams()->insert( + alter_table_streams->begin(), alter_table_streams->end()); + alter_table_streams->clear(); + auto* alter_validated_tables = alter_pb.mutable_validated_tables(); + original_pb.mutable_validated_tables()->insert( + alter_validated_tables->begin(), alter_validated_tables->end()); + alter_validated_tables->clear(); + if (alter_lock.mutable_data()->IsDbScoped()) { + auto* alter_namespace_info = alter_pb.mutable_db_scoped_info()->mutable_namespace_infos(); + original_pb.mutable_db_scoped_info()->mutable_namespace_infos()->MergeFrom( + *alter_namespace_info); + alter_namespace_info->Clear(); + } + + alter_pb.set_state(SysUniverseReplicationEntryPB::DELETED); + + if (PREDICT_FALSE(FLAGS_TEST_exit_unfinished_merging)) { + return Status::OK(); + } + + if (on_success_cb) { + RETURN_NOT_OK(on_success_cb()); + } + + { + // Need all three updates to be atomic. + auto s = CheckStatus( + sys_catalog_.Upsert( + epoch_, original_universe.get(), universe.get(), cluster_config.get()), + "Updating universe replication entries and cluster config in sys-catalog"); + } + + xcluster_manager_.SyncConsumerReplicationStatusMap( + original_universe->ReplicationGroupId(), *pm); + xcluster_manager_.SyncConsumerReplicationStatusMap(universe->ReplicationGroupId(), *pm); + + alter_lock.Commit(); + cl.Commit(); + original_lock.Commit(); + } + + // Add alter temp universe to GC. + catalog_manager_.MarkUniverseForCleanup(universe->ReplicationGroupId()); + + LOG(INFO) << "Done with Merging " << universe->id() << " into " << original_universe->id(); + + xcluster_manager_.CreateXClusterSafeTimeTableAndStartService(); + + return Status::OK(); +} + +Result> +SetupUniverseReplicationHelper::CreateUniverseReplicationInfo( + const xcluster::ReplicationGroupId& replication_group_id, + const google::protobuf::RepeatedPtrField& master_addresses, + const std::vector& producer_namespace_ids, + const std::vector& consumer_namespace_ids, + const google::protobuf::RepeatedPtrField& table_ids, bool transactional) { + SCHECK_EQ( + producer_namespace_ids.size(), consumer_namespace_ids.size(), InvalidArgument, + "We should have the namespaceIds from both producer and consumer"); + + SCHECK( + catalog_manager_.GetUniverseReplication(replication_group_id) == nullptr, AlreadyPresent, + "Replication group $0 already present", replication_group_id.ToString()); + + for (const auto& universe : catalog_manager_.GetAllUniverseReplications()) { + for (const auto& consumer_namespace_id : consumer_namespace_ids) { + SCHECK_FORMAT( + !IncludesConsumerNamespace(*universe, consumer_namespace_id), AlreadyPresent, + "Namespace $0 already included in replication group $1", consumer_namespace_id, + universe->ReplicationGroupId()); + } + } + + // Create an entry in the system catalog DocDB for this new universe replication. + scoped_refptr ri = new UniverseReplicationInfo(replication_group_id); + ri->mutable_metadata()->StartMutation(); + SysUniverseReplicationEntryPB* metadata = &ri->mutable_metadata()->mutable_dirty()->pb; + metadata->set_replication_group_id(replication_group_id.ToString()); + metadata->mutable_producer_master_addresses()->CopyFrom(master_addresses); + + if (!producer_namespace_ids.empty()) { + auto* db_scoped_info = metadata->mutable_db_scoped_info(); + for (size_t i = 0; i < producer_namespace_ids.size(); i++) { + auto* ns_info = db_scoped_info->mutable_namespace_infos()->Add(); + ns_info->set_producer_namespace_id(producer_namespace_ids[i]); + ns_info->set_consumer_namespace_id(consumer_namespace_ids[i]); + } + } + metadata->mutable_tables()->CopyFrom(table_ids); + metadata->set_state(SysUniverseReplicationEntryPB::INITIALIZING); + metadata->set_transactional(transactional); + + RETURN_NOT_OK(CheckLeaderStatus( + sys_catalog_.Upsert(epoch_, ri), "inserting universe replication info into sys-catalog")); + + // Commit the in-memory state now that it's added to the persistent catalog. + ri->mutable_metadata()->CommitMutation(); + LOG(INFO) << "Setup universe replication from producer " << ri->ToString(); + + catalog_manager_.InsertNewUniverseReplication(*ri); + + // Make sure the AutoFlags are compatible. + // This is done after the replication info is persisted since it performs RPC calls to source + // universe and we can crash during this call. + // TODO: When new master starts it can retry this step or mark the replication group as failed. + if (FLAGS_enable_xcluster_auto_flag_validation) { + const auto auto_flags_config = master_.GetAutoFlagsConfig(); + auto status = ResultToStatus(GetAutoFlagConfigVersionIfCompatible(*ri, auto_flags_config)); + + if (!status.ok()) { + MarkUniverseReplicationFailed(ri, status); + return status.CloneAndAddErrorCode(MasterError(MasterErrorPB::INVALID_REQUEST)); + } + + auto l = ri->LockForWrite(); + l.mutable_data()->pb.set_validated_local_auto_flags_config_version( + auto_flags_config.config_version()); + + RETURN_NOT_OK(CheckLeaderStatus( + sys_catalog_.Upsert(epoch_, ri), "inserting universe replication info into sys-catalog")); + + l.Commit(); + } + return ri; +} + +} // namespace yb::master diff --git a/src/yb/master/xcluster/xcluster_universe_replication_setup_helper.h b/src/yb/master/xcluster/xcluster_universe_replication_setup_helper.h new file mode 100644 index 000000000000..3ec7f4a946ce --- /dev/null +++ b/src/yb/master/xcluster/xcluster_universe_replication_setup_helper.h @@ -0,0 +1,189 @@ +// Copyright (c) YugabyteDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations +// under the License. +// + +#pragma once + +#include "yb/cdc/xcluster_types.h" +#include "yb/cdc/xrepl_types.h" +#include "yb/common/common_fwd.h" +#include "yb/common/entity_ids_types.h" +#include "yb/gutil/ref_counted.h" +#include "yb/master/leader_epoch.h" +#include "yb/master/master_fwd.h" +#include "yb/util/cow_object.h" +#include "yb/util/status_fwd.h" +#include + +namespace yb { + +class HostPortPB; + +namespace rpc { +class RpcContext; +} // namespace rpc + +namespace client { +struct YBTableInfo; +class YBTableName; +} // namespace client + +namespace master { + +class GetTableSchemaResponsePB; +struct PersistentUniverseReplicationInfo; +class SetupUniverseReplicationRequestPB; +class SetupUniverseReplicationResponsePB; +class SysCatalogTable; +class UniverseReplicationInfo; +class XClusterRpcTasks; + +// Helper class to handle SetupUniverseReplication RPC. +// This object will only live as long as the operation is in progress. +class SetupUniverseReplicationHelper : public RefCountedThreadSafe { + public: + ~SetupUniverseReplicationHelper(); + + static Status Setup( + Master& master, CatalogManager& catalog_manager, const SetupUniverseReplicationRequestPB* req, + SetupUniverseReplicationResponsePB* resp, const LeaderEpoch& epoch); + + static Status ValidateMasterAddressesBelongToDifferentCluster( + Master& master, const google::protobuf::RepeatedPtrField& master_addresses); + + private: + SetupUniverseReplicationHelper( + Master& master, CatalogManager& catalog_manager, const LeaderEpoch& epoch); + + struct SetupReplicationInfo { + std::unordered_map table_bootstrap_ids; + bool transactional; + }; + + // Helper container to track colocationId and the producer to consumer schema version mapping. + typedef std::vector> + ColocationSchemaVersions; + + typedef std::vector< + std::tuple>> + StreamUpdateInfos; + + Status SetupUniverseReplication( + const SetupUniverseReplicationRequestPB* req, SetupUniverseReplicationResponsePB* resp); + + void MarkUniverseReplicationFailed( + scoped_refptr universe, const Status& failure_status); + // Sets the appropriate failure state and the error status on the universe and commits the + // mutation to the sys catalog. + void MarkUniverseReplicationFailed( + const Status& failure_status, CowWriteLock* universe_lock, + scoped_refptr universe); + + void GetTableSchemaCallback( + const xcluster::ReplicationGroupId& replication_group_id, + const std::shared_ptr& producer_info, + const SetupReplicationInfo& setup_info, const Status& s); + + Status GetTablegroupSchemaCallbackInternal( + scoped_refptr& universe, + const std::vector& infos, const TablegroupId& producer_tablegroup_id, + const SetupReplicationInfo& setup_info, const Status& s); + + void GetTablegroupSchemaCallback( + const xcluster::ReplicationGroupId& replication_group_id, + const std::shared_ptr>& infos, + const TablegroupId& producer_tablegroup_id, const SetupReplicationInfo& setup_info, + const Status& s); + + void GetColocatedTabletSchemaCallback( + const xcluster::ReplicationGroupId& replication_group_id, + const std::shared_ptr>& info, + const SetupReplicationInfo& setup_info, const Status& s); + + Status ValidateTableAndCreateStreams( + scoped_refptr universe, + const std::shared_ptr& producer_info, + const SetupReplicationInfo& setup_info); + + // Validates a single table's schema with the corresponding table on the consumer side, and + // updates consumer_table_id with the new table id. Return the consumer table schema if the + // validation is successful. + Status ValidateTableSchemaForXCluster( + const client::YBTableInfo& info, const SetupReplicationInfo& setup_info, + GetTableSchemaResponsePB* resp); + + // Consumer API: Find out if bootstrap is required for the Producer tables. + Status IsBootstrapRequiredOnProducer( + scoped_refptr universe, const TableId& producer_table, + const std::unordered_map& table_bootstrap_ids); + + Status AddValidatedTableAndCreateStreams( + scoped_refptr universe, + const std::unordered_map& table_bootstrap_ids, + const TableId& producer_table, const TableId& consumer_table, + const ColocationSchemaVersions& colocated_schema_versions); + + // Adds a validated table to the sys catalog table map for the given universe + Status AddValidatedTableToUniverseReplication( + scoped_refptr universe, const TableId& producer_table, + const TableId& consumer_table, const SchemaVersion& producer_schema_version, + const SchemaVersion& consumer_schema_version, + const ColocationSchemaVersions& colocated_schema_versions); + + // If all tables have been validated, creates a CDC stream for each table. + Status CreateStreamsIfReplicationValidated( + scoped_refptr universe, + const std::unordered_map& table_bootstrap_ids); + + void GetStreamCallback( + const xrepl::StreamId& bootstrap_id, std::shared_ptr table_id, + std::shared_ptr> options, + const xcluster::ReplicationGroupId& replication_group_id, const TableId& table, + std::shared_ptr xcluster_rpc, const Status& s, + std::shared_ptr stream_update_infos, + std::shared_ptr update_infos_lock); + + void AddStreamToUniverseAndInitConsumer( + const xcluster::ReplicationGroupId& replication_group_id, const TableId& table, + const Result& stream_id, std::function on_success_cb = nullptr); + + Status AddStreamToUniverseAndInitConsumerInternal( + scoped_refptr universe, const TableId& table, + const xrepl::StreamId& stream_id, std::function on_success_cb); + + Status InitXClusterConsumer( + const std::vector& consumer_info, const std::string& master_addrs, + UniverseReplicationInfo& replication_info, + std::shared_ptr xcluster_rpc_tasks); + + Status MergeUniverseReplication( + scoped_refptr info, xcluster::ReplicationGroupId original_id, + std::function on_success_cb); + + Result> CreateUniverseReplicationInfo( + const xcluster::ReplicationGroupId& replication_group_id, + const google::protobuf::RepeatedPtrField& master_addresses, + const std::vector& producer_namespace_ids, + const std::vector& consumer_namespace_ids, + const google::protobuf::RepeatedPtrField& table_ids, bool transactional); + + Master& master_; + CatalogManager& catalog_manager_; + SysCatalogTable& sys_catalog_; + XClusterManager& xcluster_manager_; + const LeaderEpoch epoch_; + + DISALLOW_COPY_AND_ASSIGN(SetupUniverseReplicationHelper); +}; + +} // namespace master +} // namespace yb diff --git a/src/yb/master/xrepl_catalog_manager.cc b/src/yb/master/xrepl_catalog_manager.cc index 322caab04986..40f079ba43f3 100644 --- a/src/yb/master/xrepl_catalog_manager.cc +++ b/src/yb/master/xrepl_catalog_manager.cc @@ -28,9 +28,6 @@ #include "yb/docdb/docdb_pgapi.h" -#include "yb/gutil/bind.h" -#include "yb/gutil/bind_helpers.h" - #include "yb/master/catalog_entity_info.h" #include "yb/master/catalog_manager-internal.h" #include "yb/master/catalog_manager.h" @@ -45,12 +42,10 @@ #include "yb/master/master_replication.pb.h" #include "yb/master/master_util.h" #include "yb/master/snapshot_transfer_manager.h" -#include "yb/master/ysql_tablegroup_manager.h" #include "yb/util/backoff_waiter.h" #include "yb/util/debug-util.h" #include "yb/util/scope_exit.h" -#include "yb/util/string_util.h" #include "yb/util/sync_point.h" #include "yb/util/thread.h" #include "yb/util/trace.h" @@ -64,9 +59,6 @@ using std::vector; DEFINE_RUNTIME_uint32(cdc_wal_retention_time_secs, 4 * 3600, "WAL retention time in seconds to be used for tables for which a CDC stream was created."); -DEFINE_RUNTIME_bool(check_bootstrap_required, false, - "Is it necessary to check whether bootstrap is required for Universe Replication."); - DEFINE_test_flag(bool, disable_cdc_state_insert_on_setup, false, "Disable inserting new entries into cdc state as part of the setup flow."); @@ -82,18 +74,6 @@ DEFINE_RUNTIME_int32(wait_replication_drain_retry_timeout_ms, 2000, "Timeout in milliseconds in between CheckReplicationDrain calls to tservers " "in case of retries."); -DEFINE_RUNTIME_int32(ns_replication_sync_retry_secs, 5, - "Frequency at which the bg task will try to sync with producer and add tables to " - "the current NS-level replication, when there are non-replicated consumer tables."); - -DEFINE_RUNTIME_int32(ns_replication_sync_backoff_secs, 60, - "Frequency of the add table task for a NS-level replication, when there are no " - "non-replicated consumer tables."); - -DEFINE_RUNTIME_int32(ns_replication_sync_error_backoff_secs, 300, - "Frequency of the add table task for a NS-level replication, when there are too " - "many consecutive errors happening for the replication."); - DEFINE_RUNTIME_bool(disable_universe_gc, false, "Whether to run the GC on universes or not."); DEFINE_RUNTIME_int32(cdc_parent_tablet_deletion_task_retry_secs, 30, @@ -106,24 +86,6 @@ DEFINE_NON_RUNTIME_uint32(max_replication_slots, 10, DEFINE_test_flag(bool, hang_wait_replication_drain, false, "Used in tests to temporarily block WaitForReplicationDrain."); -DEFINE_test_flag(bool, exit_unfinished_deleting, false, - "Whether to exit part way through the deleting universe process."); - -DEFINE_test_flag(bool, exit_unfinished_merging, false, - "Whether to exit part way through the merging universe process."); - -DEFINE_test_flag( - bool, xcluster_fail_create_consumer_snapshot, false, - "In the SetupReplicationWithBootstrap flow, test failure to create snapshot on consumer."); - -DEFINE_test_flag( - bool, xcluster_fail_restore_consumer_snapshot, false, - "In the SetupReplicationWithBootstrap flow, test failure to restore snapshot on consumer."); - -DEFINE_test_flag( - bool, allow_ycql_transactional_xcluster, false, - "Determines if xCluster transactional replication on YCQL tables is allowed."); - DEFINE_RUNTIME_AUTO_bool(cdc_enable_postgres_replica_identity, kLocalPersisted, false, true, "Enable new record types in CDC streams"); @@ -133,9 +95,6 @@ DEFINE_RUNTIME_bool(enable_backfilling_cdc_stream_with_replication_slot, false, "Intended to be used for making CDC streams created before replication slot support work with" " the replication slot commands."); -DEFINE_test_flag(bool, fail_universe_replication_merge, false, "Causes MergeUniverseReplication to " - "fail with an error."); - DEFINE_test_flag(bool, xcluster_fail_setup_stream_update, false, "Fail UpdateCDCStream RPC call"); DEFINE_RUNTIME_AUTO_bool(cdcsdk_enable_dynamic_tables_disable_option, @@ -175,7 +134,6 @@ TAG_FLAG(cdcsdk_enable_identification_of_non_eligible_tables, hidden); DECLARE_int32(master_rpc_timeout_ms); DECLARE_bool(ysql_yb_enable_replication_commands); DECLARE_bool(yb_enable_cdc_consistent_snapshot_streams); -DECLARE_bool(enable_xcluster_auto_flag_validation); DECLARE_bool(ysql_yb_enable_replica_identity); @@ -183,51 +141,15 @@ DECLARE_bool(ysql_yb_enable_replica_identity); #define RETURN_ACTION_NOT_OK(expr, action) \ RETURN_NOT_OK_PREPEND((expr), Format("An error occurred while $0", action)) -// assumes the existence of a local variable bootstrap_info -#define MARK_BOOTSTRAP_FAILED_NOT_OK(s) \ - do { \ - auto&& _s = (s); \ - if (PREDICT_FALSE(!_s.ok())) { \ - MarkReplicationBootstrapFailed(bootstrap_info, _s); \ - return; \ - } \ - } while (false) - -#define VERIFY_RESULT_MARK_BOOTSTRAP_FAILED(expr) \ - RESULT_CHECKER_HELPER(expr, MARK_BOOTSTRAP_FAILED_NOT_OK(ResultToStatus(__result))) - #define RETURN_INVALID_REQUEST_STATUS(error_msg) \ return STATUS( \ InvalidArgument, error_msg, \ MasterError(MasterErrorPB::INVALID_REQUEST)) -#define SET_CDCSDK_STREAM_CREATION_STATE(value) \ - if (mode == CreateNewCDCStreamMode::kCdcsdkNamespaceAndTableIds) { \ - cdcsdk_stream_creation_state = value; \ - } \ - namespace yb { using client::internal::RemoteTabletServer; namespace master { -using TableMetaPB = ImportSnapshotMetaResponsePB::TableMetaPB; - -namespace { - -template -Status ValidateUniverseUUID(const RequestType& req, CatalogManager& catalog_manager) { - if (req->has_universe_uuid() && !req->universe_uuid().empty()) { - auto universe_uuid = catalog_manager.GetUniverseUuidIfExists(); - SCHECK( - universe_uuid && universe_uuid->ToString() == req->universe_uuid(), InvalidArgument, - "Invalid Universe UUID $0. Expected $1", req->universe_uuid(), - (universe_uuid ? universe_uuid->ToString() : "empty")); - } - - return Status::OK(); -} - -} // namespace //////////////////////////////////////////////////////////// // CDC Stream Loader @@ -593,19 +515,6 @@ std::string CDCStreamInfosAsString(const std::vector& cdc_ return AsString(cdc_stream_ids); } -Status ReturnErrorOrAddWarning( - const Status& s, bool ignore_errors, DeleteUniverseReplicationResponsePB* resp) { - if (!s.ok()) { - if (ignore_errors) { - // Continue executing, save the status as a warning. - AppStatusPB* warning = resp->add_warnings(); - StatusToPB(s, warning); - return Status::OK(); - } - return s.CloneAndAppend("\nUse 'ignore-errors' to ignore this error."); - } - return s; -} } // namespace Status CatalogManager::DropXClusterStreamsOfTables(const std::unordered_set& table_ids) { @@ -3040,1999 +2949,230 @@ Status CatalogManager::IsBootstrapRequired( return Status::OK(); } -Result> -CatalogManager::CreateUniverseReplicationInfoForProducer( - const xcluster::ReplicationGroupId& replication_group_id, - const google::protobuf::RepeatedPtrField& master_addresses, - const std::vector& producer_namespace_ids, - const std::vector& consumer_namespace_ids, - const google::protobuf::RepeatedPtrField& table_ids, bool transactional) { - SCHECK_EQ( - producer_namespace_ids.size(), consumer_namespace_ids.size(), InvalidArgument, - "We should have the namespaceIds from both producer and consumer"); - - scoped_refptr ri; - { - TRACE("Acquired catalog manager lock"); - SharedLock lock(mutex_); - - if (FindPtrOrNull(universe_replication_map_, replication_group_id) != nullptr) { - return STATUS( - AlreadyPresent, "Replication group already present", replication_group_id.ToString()); - } +Status CatalogManager::IsTableBootstrapRequired( + const TableId& table_id, + const xrepl::StreamId& stream_id, + CoarseTimePoint deadline, + bool* const bootstrap_required) { + scoped_refptr table = VERIFY_RESULT(FindTableById(table_id)); + RSTATUS_DCHECK(table != nullptr, NotFound, "Table ID not found: " + table_id); - for (const auto& [universe_rg_id, universe] : universe_replication_map_) { - for (const auto& consumer_namespace_id : consumer_namespace_ids) { - SCHECK_FORMAT( - !IncludesConsumerNamespace(*universe, consumer_namespace_id), AlreadyPresent, - "Namespace $0 already included in replication group $1", consumer_namespace_id, - universe_rg_id); - } - } + // Make a batch call for IsBootstrapRequired on every relevant TServer. + std::map, cdc::IsBootstrapRequiredRequestPB> + proxy_to_request; + for (const auto& tablet : VERIFY_RESULT(table->GetTablets())) { + auto ts = VERIFY_RESULT(tablet->GetLeader()); + std::shared_ptr proxy; + RETURN_NOT_OK(ts->GetProxy(&proxy)); + proxy_to_request[proxy].add_tablet_ids(tablet->id()); } - // Create an entry in the system catalog DocDB for this new universe replication. - ri = new UniverseReplicationInfo(replication_group_id); - ri->mutable_metadata()->StartMutation(); - SysUniverseReplicationEntryPB* metadata = &ri->mutable_metadata()->mutable_dirty()->pb; - metadata->set_replication_group_id(replication_group_id.ToString()); - metadata->mutable_producer_master_addresses()->CopyFrom(master_addresses); + // TODO: Make the RPCs async and parallel. + *bootstrap_required = false; + for (auto& proxy_request : proxy_to_request) { + auto& tablet_req = proxy_request.second; + cdc::IsBootstrapRequiredResponsePB tablet_resp; + rpc::RpcController rpc; + rpc.set_deadline(deadline); + if (stream_id) { + tablet_req.set_stream_id(stream_id.ToString()); + } + auto& cdc_service = proxy_request.first; - if (!producer_namespace_ids.empty()) { - auto* db_scoped_info = metadata->mutable_db_scoped_info(); - for (size_t i = 0; i < producer_namespace_ids.size(); i++) { - auto* ns_info = db_scoped_info->mutable_namespace_infos()->Add(); - ns_info->set_producer_namespace_id(producer_namespace_ids[i]); - ns_info->set_consumer_namespace_id(consumer_namespace_ids[i]); + RETURN_NOT_OK(cdc_service->IsBootstrapRequired(tablet_req, &tablet_resp, &rpc)); + if (tablet_resp.has_error()) { + RETURN_NOT_OK(StatusFromPB(tablet_resp.error().status())); + } else if (tablet_resp.has_bootstrap_required() && tablet_resp.bootstrap_required()) { + *bootstrap_required = true; + break; } } - metadata->mutable_tables()->CopyFrom(table_ids); - metadata->set_state(SysUniverseReplicationEntryPB::INITIALIZING); - metadata->set_transactional(transactional); - - RETURN_NOT_OK(CheckLeaderStatus( - sys_catalog_->Upsert(leader_ready_term(), ri), - "inserting universe replication info into sys-catalog")); - - TRACE("Wrote universe replication info to sys-catalog"); - // Commit the in-memory state now that it's added to the persistent catalog. - ri->mutable_metadata()->CommitMutation(); - LOG(INFO) << "Setup universe replication from producer " << ri->ToString(); - { - LockGuard lock(mutex_); - universe_replication_map_[ri->ReplicationGroupId()] = ri; - } + return Status::OK(); +} - // Make sure the AutoFlags are compatible. - // This is done after the replication info is persisted since it performs RPC calls to source - // universe and we can crash during this call. - // TODO: When new master starts it can retry this step or mark the replication group as failed. - if (FLAGS_enable_xcluster_auto_flag_validation) { - const auto auto_flags_config = master_->GetAutoFlagsConfig(); - auto status = ResultToStatus(GetAutoFlagConfigVersionIfCompatible(*ri, auto_flags_config)); +Status CatalogManager::UpdateCDCProducerOnTabletSplit( + const TableId& producer_table_id, const SplitTabletIds& split_tablet_ids) { + std::vector streams; + std::vector entries; + for (const auto stream_type : {cdc::XCLUSTER, cdc::CDCSDK}) { + if (stream_type == cdc::CDCSDK && + FLAGS_cdcsdk_enable_cleanup_of_non_eligible_tables_from_stream) { + const auto& table_info = GetTableInfo(producer_table_id); + // Skip adding children tablet entries in cdc state if the table is an index or a mat view. + // These tables, if present in CDC stream, are anyway going to be removed by a bg thread. This + // check ensures even if there is a race condition where a tablet of a non-eligible table + // splits and concurrently we are removing such tables from stream, the child tables do not + // get added. + { + SharedLock lock(mutex_); + if (!IsTableEligibleForCDCSDKStream(table_info, std::nullopt)) { + LOG(INFO) << "Skipping adding children tablets to cdc state for table " + << producer_table_id << " as it is not meant to part of a CDC stream"; + continue; + } + } + } - if (!status.ok()) { - MarkUniverseReplicationFailed(ri, status); - return status.CloneAndAddErrorCode(MasterError(MasterErrorPB::INVALID_REQUEST)); + { + SharedLock lock(mutex_); + streams = GetXReplStreamsForTable(producer_table_id, stream_type); } - auto l = ri->LockForWrite(); - l.mutable_data()->pb.set_validated_local_auto_flags_config_version( - auto_flags_config.config_version()); + for (const auto& stream : streams) { + auto last_active_time = GetCurrentTimeMicros(); - RETURN_NOT_OK(CheckLeaderStatus( - sys_catalog_->Upsert(leader_ready_term(), ri), - "inserting universe replication info into sys-catalog")); + std::optional parent_entry_opt; + if (stream_type == cdc::CDCSDK) { + parent_entry_opt = VERIFY_RESULT(cdc_state_table_->TryFetchEntry( + {split_tablet_ids.source, stream->StreamId()}, + cdc::CDCStateTableEntrySelector().IncludeActiveTime().IncludeCDCSDKSafeTime())); + DCHECK(parent_entry_opt); + } - l.Commit(); - } - return ri; -} + // In the case of a Consistent Snapshot Stream, set the active_time of the children tablets + // to the corresponding value in the parent tablet. + // This will allow to establish that a child tablet is of interest to a stream + // iff the parent tablet is also of interest to the stream. + // Thus, retention barriers, inherited from the parent tablet, can be released + // on the children tablets also if not of interest to the stream + if (stream->IsConsistentSnapshotStream()) { + LOG_WITH_FUNC(INFO) << "Copy active time from parent to child tablets" + << " Tablets involved: " << split_tablet_ids.ToString() + << " Consistent Snapshot StreamId: " << stream->StreamId(); + DCHECK(parent_entry_opt->active_time); + if (parent_entry_opt && parent_entry_opt->active_time) { + last_active_time = *parent_entry_opt->active_time; + } else { + LOG_WITH_FUNC(WARNING) + << Format("Did not find $0 value in the cdc state table", + parent_entry_opt ? "active_time" : "row") + << " for parent tablet: " << split_tablet_ids.source + << " and stream: " << stream->StreamId(); + } + } -Result> -CatalogManager::CreateUniverseReplicationBootstrapInfoForProducer( - const xcluster::ReplicationGroupId& replication_group_id, - const google::protobuf::RepeatedPtrField& master_addresses, - const LeaderEpoch& epoch, bool transactional) { - scoped_refptr bootstrap_info; - { - TRACE("Acquired catalog manager lock"); - SharedLock lock(mutex_); + // Insert children entries into cdc_state now, set the opid to 0.0 and the timestamp to + // NULL. When we process the parent's SPLIT_OP in GetChanges, we will update the opid to + // the SPLIT_OP so that the children pollers continue from the next records. When we process + // the first GetChanges for the children, then their timestamp value will be set. We use + // this information to know that the children has been polled for. Once both children have + // been polled for, then we can delete the parent tablet via the bg task + // DoProcessXClusterParentTabletDeletion. + for (const auto& child_tablet_id : + {split_tablet_ids.children.first, split_tablet_ids.children.second}) { + cdc::CDCStateTableEntry entry(child_tablet_id, stream->StreamId()); + entry.checkpoint = OpId().Min(); - if (FindPtrOrNull(universe_replication_bootstrap_map_, replication_group_id) != nullptr) { - return STATUS( - InvalidArgument, "Bootstrap already present", replication_group_id.ToString(), - MasterError(MasterErrorPB::INVALID_REQUEST)); + if (stream_type == cdc::CDCSDK) { + entry.active_time = last_active_time; + DCHECK(parent_entry_opt->cdc_sdk_safe_time); + if (parent_entry_opt && parent_entry_opt->cdc_sdk_safe_time) { + entry.cdc_sdk_safe_time = *parent_entry_opt->cdc_sdk_safe_time; + } else { + LOG_WITH_FUNC(WARNING) << Format( + "Did not find $0 value in the cdc state table", + parent_entry_opt ? "cdc_sdk_safe_time" : "row") + << " for parent tablet: " << split_tablet_ids.source + << " and stream: " << stream->StreamId(); + entry.cdc_sdk_safe_time = last_active_time; + } + } + + entries.push_back(std::move(entry)); + } } } - // Create an entry in the system catalog DocDB for this new universe replication. - bootstrap_info = new UniverseReplicationBootstrapInfo(replication_group_id); - bootstrap_info->mutable_metadata()->StartMutation(); - - SysUniverseReplicationBootstrapEntryPB* metadata = - &bootstrap_info->mutable_metadata()->mutable_dirty()->pb; - metadata->set_replication_group_id(replication_group_id.ToString()); - metadata->mutable_producer_master_addresses()->CopyFrom(master_addresses); - metadata->set_state(SysUniverseReplicationBootstrapEntryPB::INITIALIZING); - metadata->set_transactional(transactional); - metadata->set_leader_term(epoch.leader_term); - metadata->set_pitr_count(epoch.pitr_count); + return cdc_state_table_->InsertEntries(entries); +} - RETURN_NOT_OK(CheckLeaderStatus( - sys_catalog_->Upsert(leader_ready_term(), bootstrap_info), - "inserting universe replication bootstrap info into sys-catalog")); +Status CatalogManager::ChangeXClusterRole( + const ChangeXClusterRoleRequestPB* req, + ChangeXClusterRoleResponsePB* resp, + rpc::RpcContext* rpc) { + LOG(INFO) << "Servicing ChangeXClusterRole request from " << RequestorString(rpc) << ": " + << req->ShortDebugString(); + return Status::OK(); +} - TRACE("Wrote universe replication bootstrap info to sys-catalog"); - // Commit the in-memory state now that it's added to the persistent catalog. - bootstrap_info->mutable_metadata()->CommitMutation(); - LOG(INFO) << "Setup universe replication bootstrap from producer " << bootstrap_info->ToString(); +Status CatalogManager::BootstrapProducer( + const BootstrapProducerRequestPB* req, + BootstrapProducerResponsePB* resp, + rpc::RpcContext* rpc) { + LOG(INFO) << "Servicing BootstrapProducer request from " << RequestorString(rpc) << ": " + << req->ShortDebugString(); - { - LockGuard lock(mutex_); - universe_replication_bootstrap_map_[bootstrap_info->ReplicationGroupId()] = bootstrap_info; + const bool pg_database_type = req->db_type() == YQL_DATABASE_PGSQL; + SCHECK( + pg_database_type || req->db_type() == YQL_DATABASE_CQL, InvalidArgument, + "Invalid database type"); + SCHECK( + req->has_namespace_name() && !req->namespace_name().empty(), InvalidArgument, + "No namespace specified"); + SCHECK_GT(req->table_name_size(), 0, InvalidArgument, "No tables specified"); + if (pg_database_type) { + SCHECK_EQ( + req->pg_schema_name_size(), req->table_name_size(), InvalidArgument, + "Number of tables and number of pg schemas must match"); + } else { + SCHECK_EQ( + req->pg_schema_name_size(), 0, InvalidArgument, + "Pg Schema does not apply to CQL databases"); } - return bootstrap_info; -} -Status CatalogManager::ValidateMasterAddressesBelongToDifferentCluster( - const google::protobuf::RepeatedPtrField& master_addresses) { - std::vector cluster_master_addresses; - RETURN_NOT_OK(master_->ListMasters(&cluster_master_addresses)); - std::unordered_set cluster_master_hps; - - for (const auto& cluster_elem : cluster_master_addresses) { - if (cluster_elem.has_registration()) { - auto p_rpc_addresses = cluster_elem.registration().private_rpc_addresses(); - for (const auto& p_rpc_elem : p_rpc_addresses) { - cluster_master_hps.insert(HostPort::FromPB(p_rpc_elem)); - } + cdc::BootstrapProducerRequestPB bootstrap_req; + master::TSDescriptor* ts = nullptr; + for (int i = 0; i < req->table_name_size(); i++) { + string pg_schema_name = pg_database_type ? req->pg_schema_name(i) : ""; + auto table_info = GetTableInfoFromNamespaceNameAndTableName( + req->db_type(), req->namespace_name(), req->table_name(i), pg_schema_name); + SCHECK( + table_info, NotFound, Format("Table $0.$1$2 not found"), req->namespace_name(), + (pg_schema_name.empty() ? "" : pg_schema_name + "."), req->table_name(i)); - auto broadcast_addresses = cluster_elem.registration().broadcast_addresses(); - for (const auto& bc_elem : broadcast_addresses) { - cluster_master_hps.insert(HostPort::FromPB(bc_elem)); - } - } + bootstrap_req.add_table_ids(table_info->id()); + resp->add_table_ids(table_info->id()); - for (const auto& master_address : master_addresses) { - auto master_hp = HostPort::FromPB(master_address); - SCHECK( - !cluster_master_hps.contains(master_hp), InvalidArgument, - "Master address $0 belongs to the target universe", master_hp); + // Pick a valid tserver to bootstrap from. + if (!ts) { + ts = VERIFY_RESULT(VERIFY_RESULT(table_info->GetTablets()).front()->GetLeader()); } } - return Status::OK(); -} - -Result CatalogManager::DoReplicationBootstrapCreateSnapshot( - const std::vector& tables, - scoped_refptr bootstrap_info) { - LOG(INFO) << Format( - "SetupReplicationWithBootstrap: create producer snapshot for replication $0", - bootstrap_info->id()); - SetReplicationBootstrapState( - bootstrap_info, SysUniverseReplicationBootstrapEntryPB::CREATE_PRODUCER_SNAPSHOT); - - auto xcluster_rpc_tasks = VERIFY_RESULT(bootstrap_info->GetOrCreateXClusterRpcTasks( - bootstrap_info->LockForRead()->pb.producer_master_addresses())); + SCHECK(ts, IllegalState, "No valid tserver found to bootstrap from"); - TxnSnapshotId old_snapshot_id = TxnSnapshotId::Nil(); + std::shared_ptr proxy; + RETURN_NOT_OK(ts->GetProxy(&proxy)); - // Send create request and wait for completion. - auto snapshot_result = xcluster_rpc_tasks->CreateSnapshot(tables, &old_snapshot_id); + cdc::BootstrapProducerResponsePB bootstrap_resp; + rpc::RpcController bootstrap_rpc; + bootstrap_rpc.set_deadline(rpc->GetClientDeadline()); - // If the producer failed to complete the snapshot, we still want to store the snapshot_id for - // cleanup purposes. - if (!old_snapshot_id.IsNil()) { - auto l = bootstrap_info->LockForWrite(); - l.mutable_data()->set_old_snapshot_id(old_snapshot_id); + RETURN_NOT_OK(proxy->BootstrapProducer(bootstrap_req, &bootstrap_resp, &bootstrap_rpc)); + if (bootstrap_resp.has_error()) { + RETURN_NOT_OK(StatusFromPB(bootstrap_resp.error().status())); + } - // Update sys_catalog. - const Status s = sys_catalog_->Upsert(leader_ready_term(), bootstrap_info); - l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); + resp->mutable_bootstrap_ids()->Swap(bootstrap_resp.mutable_cdc_bootstrap_ids()); + if (bootstrap_resp.has_bootstrap_time()) { + resp->set_bootstrap_time(bootstrap_resp.bootstrap_time()); } - return snapshot_result; + return Status::OK(); } -Result> CatalogManager::DoReplicationBootstrapImportSnapshot( - const SnapshotInfoPB& snapshot, - scoped_refptr bootstrap_info) { - /////////////////////////// - // ImportSnapshotMeta - /////////////////////////// - LOG(INFO) << Format( - "SetupReplicationWithBootstrap: import snapshot for replication $0", bootstrap_info->id()); - SetReplicationBootstrapState( - bootstrap_info, SysUniverseReplicationBootstrapEntryPB::IMPORT_SNAPSHOT); - - NamespaceMap namespace_map; - UDTypeMap type_map; - ExternalTableSnapshotDataMap tables_data; - - // ImportSnapshotMeta timeout should be a function of the table size. - auto deadline = CoarseMonoClock::Now() + MonoDelta::FromSeconds(10 + 1 * tables_data.size()); - auto epoch = bootstrap_info->LockForRead()->epoch(); - RETURN_NOT_OK(DoImportSnapshotMeta( - snapshot, epoch, std::nullopt /* clone_target_namespace_name */, &namespace_map, - &type_map, &tables_data, deadline)); - - // Update sys catalog with new information. +Status CatalogManager::SetUniverseReplicationInfoEnabled( + const xcluster::ReplicationGroupId& replication_group_id, bool is_enabled) { + scoped_refptr universe; { - auto l = bootstrap_info->LockForWrite(); - l.mutable_data()->set_new_snapshot_objects(namespace_map, type_map, tables_data); + SharedLock lock(mutex_); - // Update sys_catalog. - const Status s = sys_catalog_->Upsert(leader_ready_term(), bootstrap_info); - l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); - } - - /////////////////////////// - // CreateConsumerSnapshot - /////////////////////////// - LOG(INFO) << Format( - "SetupReplicationWithBootstrap: create consumer snapshot for replication $0", - bootstrap_info->id()); - SetReplicationBootstrapState( - bootstrap_info, SysUniverseReplicationBootstrapEntryPB::CREATE_CONSUMER_SNAPSHOT); - - CreateSnapshotRequestPB snapshot_req; - CreateSnapshotResponsePB snapshot_resp; - - std::vector tables_meta; - for (const auto& [table_id, table_data] : tables_data) { - if (table_data.table_meta) { - tables_meta.push_back(std::move(*table_data.table_meta)); - } - } - - for (const auto& table_meta : tables_meta) { - SCHECK( - ImportSnapshotMetaResponsePB_TableType_IsValid(table_meta.table_type()), InternalError, - Format("Found unknown table type: $0", table_meta.table_type())); - - const string& new_table_id = table_meta.table_ids().new_id(); - RETURN_NOT_OK(WaitForCreateTableToFinish(new_table_id, deadline)); - - snapshot_req.mutable_tables()->Add()->set_table_id(new_table_id); - } - - snapshot_req.set_add_indexes(false); - snapshot_req.set_transaction_aware(true); - snapshot_req.set_imported(true); - RETURN_NOT_OK(CreateTransactionAwareSnapshot(snapshot_req, &snapshot_resp, deadline)); - - // Update sys catalog with new information. - { - auto l = bootstrap_info->LockForWrite(); - l.mutable_data()->set_new_snapshot_id(TryFullyDecodeTxnSnapshotId(snapshot_resp.snapshot_id())); - - // Update sys_catalog. - const Status s = sys_catalog_->Upsert(leader_ready_term(), bootstrap_info); - l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); - } - - return std::vector(tables_meta.begin(), tables_meta.end()); -} - -Status CatalogManager::DoReplicationBootstrapTransferAndRestoreSnapshot( - const std::vector& tables_meta, - scoped_refptr bootstrap_info) { - // Retrieve required data from PB. - TxnSnapshotId old_snapshot_id = TxnSnapshotId::Nil(); - TxnSnapshotId new_snapshot_id = TxnSnapshotId::Nil(); - google::protobuf::RepeatedPtrField producer_masters; - auto epoch = bootstrap_info->epoch(); - { - auto l = bootstrap_info->LockForRead(); - old_snapshot_id = l->old_snapshot_id(); - new_snapshot_id = l->new_snapshot_id(); - producer_masters.CopyFrom(l->pb.producer_master_addresses()); - } - - auto xcluster_rpc_tasks = - VERIFY_RESULT(bootstrap_info->GetOrCreateXClusterRpcTasks(producer_masters)); - - // Transfer snapshot. - SetReplicationBootstrapState( - bootstrap_info, SysUniverseReplicationBootstrapEntryPB::TRANSFER_SNAPSHOT); - auto snapshot_transfer_manager = - std::make_shared(master_, this, xcluster_rpc_tasks->client()); - RETURN_NOT_OK_PREPEND( - snapshot_transfer_manager->TransferSnapshot( - old_snapshot_id, new_snapshot_id, tables_meta, epoch), - Format("Failed to transfer snapshot $0 from producer", old_snapshot_id.ToString())); - - // Restore snapshot. - SetReplicationBootstrapState( - bootstrap_info, SysUniverseReplicationBootstrapEntryPB::RESTORE_SNAPSHOT); - auto restoration_id = VERIFY_RESULT( - snapshot_coordinator_.Restore(new_snapshot_id, HybridTime(), epoch.leader_term)); - - if (PREDICT_FALSE(FLAGS_TEST_xcluster_fail_restore_consumer_snapshot)) { - return STATUS(Aborted, "Test failure"); - } - - // Wait for restoration to complete. - return WaitFor( - [this, &new_snapshot_id, &restoration_id]() -> Result { - ListSnapshotRestorationsResponsePB resp; - RETURN_NOT_OK( - snapshot_coordinator_.ListRestorations(restoration_id, new_snapshot_id, &resp)); - - SCHECK_EQ( - resp.restorations_size(), 1, IllegalState, - Format("Expected 1 restoration, got $0", resp.restorations_size())); - const auto& restoration = *resp.restorations().begin(); - const auto& state = restoration.entry().state(); - return state == SysSnapshotEntryPB::RESTORED; - }, - MonoDelta::kMax, "Waiting for restoration to finish", 100ms); -} - -Status CatalogManager::ValidateReplicationBootstrapRequest( - const SetupNamespaceReplicationWithBootstrapRequestPB* req) { - SCHECK( - !req->replication_id().empty(), InvalidArgument, "Replication ID must be provided", - req->ShortDebugString()); - - SCHECK( - req->producer_master_addresses_size() > 0, InvalidArgument, - "Producer master address must be provided", req->ShortDebugString()); - - { - auto l = ClusterConfig()->LockForRead(); - SCHECK( - l->pb.cluster_uuid() != req->replication_id(), InvalidArgument, - "Replication name cannot be the target universe UUID", req->ShortDebugString()); - } - - RETURN_NOT_OK_PREPEND( - ValidateMasterAddressesBelongToDifferentCluster(req->producer_master_addresses()), - req->ShortDebugString()); - - GetUniverseReplicationRequestPB universe_req; - GetUniverseReplicationResponsePB universe_resp; - universe_req.set_replication_group_id(req->replication_id()); - SCHECK( - GetUniverseReplication(&universe_req, &universe_resp, /* RpcContext */ nullptr).IsNotFound(), - InvalidArgument, Format("Can't bootstrap replication that already exists")); - - return Status::OK(); -} - -void CatalogManager::DoReplicationBootstrap( - const xcluster::ReplicationGroupId& replication_id, - const std::vector& tables, - Result bootstrap_producer_result) { - // First get the universe. - scoped_refptr bootstrap_info; - { - SharedLock lock(mutex_); - TRACE("Acquired catalog manager lock"); - - bootstrap_info = FindPtrOrNull(universe_replication_bootstrap_map_, replication_id); - if (bootstrap_info == nullptr) { - LOG(ERROR) << "UniverseReplicationBootstrap not found: " << replication_id; - return; - } - } - - // Verify the result from BootstrapProducer & update values in PB if successful. - auto table_bootstrap_ids = - VERIFY_RESULT_MARK_BOOTSTRAP_FAILED(std::move(bootstrap_producer_result)); - { - auto l = bootstrap_info->LockForWrite(); - auto map = l.mutable_data()->pb.mutable_table_bootstrap_ids(); - for (const auto& [table_id, bootstrap_id] : table_bootstrap_ids) { - (*map)[table_id] = bootstrap_id.ToString(); - } - - // Update sys_catalog. - const Status s = sys_catalog_->Upsert(leader_ready_term(), bootstrap_info); - l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); - } - - // Create producer snapshot. - auto snapshot = VERIFY_RESULT_MARK_BOOTSTRAP_FAILED( - DoReplicationBootstrapCreateSnapshot(tables, bootstrap_info)); - - // Import snapshot and create consumer snapshot. - auto tables_meta = VERIFY_RESULT_MARK_BOOTSTRAP_FAILED( - DoReplicationBootstrapImportSnapshot(snapshot, bootstrap_info)); - - // Transfer and restore snapshot. - MARK_BOOTSTRAP_FAILED_NOT_OK( - DoReplicationBootstrapTransferAndRestoreSnapshot(tables_meta, bootstrap_info)); - - // Call SetupUniverseReplication - SetupUniverseReplicationRequestPB replication_req; - SetupUniverseReplicationResponsePB replication_resp; - { - auto l = bootstrap_info->LockForRead(); - replication_req.set_replication_group_id(l->pb.replication_group_id()); - replication_req.set_transactional(l->pb.transactional()); - replication_req.mutable_producer_master_addresses()->CopyFrom( - l->pb.producer_master_addresses()); - for (const auto& [table_id, bootstrap_id] : table_bootstrap_ids) { - replication_req.add_producer_table_ids(table_id); - replication_req.add_producer_bootstrap_ids(bootstrap_id.ToString()); - } - } - - SetReplicationBootstrapState( - bootstrap_info, SysUniverseReplicationBootstrapEntryPB::SETUP_REPLICATION); - MARK_BOOTSTRAP_FAILED_NOT_OK( - SetupUniverseReplication(&replication_req, &replication_resp, /* rpc = */ nullptr)); - - LOG(INFO) << Format( - "Successfully completed replication bootstrap for $0", replication_id.ToString()); - SetReplicationBootstrapState(bootstrap_info, SysUniverseReplicationBootstrapEntryPB::DONE); -} - -/* - * SetupNamespaceReplicationWithBootstrap is setup in 5 stages. - * 1. Validates user input & connect to producer. - * 2. Calls BootstrapProducer with all user tables in namespace. - * 3. Create snapshot on producer and import onto consumer. - * 4. Download snapshots from producer and restore on consumer. - * 5. SetupUniverseReplication. - */ -Status CatalogManager::SetupNamespaceReplicationWithBootstrap( - const SetupNamespaceReplicationWithBootstrapRequestPB* req, - SetupNamespaceReplicationWithBootstrapResponsePB* resp, - rpc::RpcContext* rpc, - const LeaderEpoch& epoch) { - LOG(INFO) << Format( - "SetupNamespaceReplicationWithBootstrap from $0: $1", RequestorString(rpc), - req->DebugString()); - - // PHASE 1: Validating user input. - RETURN_NOT_OK(ValidateReplicationBootstrapRequest(req)); - - // Create entry in sys catalog. - auto replication_id = xcluster::ReplicationGroupId(req->replication_id()); - auto transactional = req->has_transactional() ? req->transactional() : false; - auto bootstrap_info = VERIFY_RESULT(CreateUniverseReplicationBootstrapInfoForProducer( - replication_id, req->producer_master_addresses(), epoch, transactional)); - - // Connect to producer. - auto xcluster_rpc_result = - bootstrap_info->GetOrCreateXClusterRpcTasks(req->producer_master_addresses()); - if (!xcluster_rpc_result.ok()) { - auto s = ResultToStatus(xcluster_rpc_result); - MarkReplicationBootstrapFailed(bootstrap_info, s); - return s; - } - auto xcluster_rpc_tasks = std::move(*xcluster_rpc_result); - - // Get user tables in producer namespace. - auto tables_result = xcluster_rpc_tasks->client()->ListUserTables(req->producer_namespace()); - if (!tables_result.ok()) { - auto s = ResultToStatus(tables_result); - MarkReplicationBootstrapFailed(bootstrap_info, s); - return s; - } - auto tables = std::move(*tables_result); - - // Bootstrap producer. - SetReplicationBootstrapState( - bootstrap_info, SysUniverseReplicationBootstrapEntryPB::BOOTSTRAP_PRODUCER); - auto s = xcluster_rpc_tasks->BootstrapProducer( - req->producer_namespace(), tables, - Bind(&CatalogManager::DoReplicationBootstrap, Unretained(this), replication_id, tables)); - if (!s.ok()) { - MarkReplicationBootstrapFailed(bootstrap_info, s); - return s; - } - - return Status::OK(); -} - -/* - * UniverseReplication is setup in 4 stages within the Catalog Manager - * 1. SetupUniverseReplication: Validates user input & requests Producer schema. - * 2. GetTableSchemaCallback: Validates Schema compatibility & requests Producer CDC init. - * 3. AddCDCStreamToUniverseAndInitConsumer: Setup RPC connections for CDC Streaming - * 4. InitXClusterConsumer: Initializes the Consumer settings to begin tailing data - */ -Status CatalogManager::SetupUniverseReplication( - const SetupUniverseReplicationRequestPB* req, - SetupUniverseReplicationResponsePB* resp, - rpc::RpcContext* rpc) { - LOG(INFO) << "SetupUniverseReplication from " << RequestorString(rpc) << ": " - << req->DebugString(); - - // Sanity checking section. - if (!req->has_replication_group_id()) { - return STATUS( - InvalidArgument, "Producer universe ID must be provided", req->ShortDebugString(), - MasterError(MasterErrorPB::INVALID_REQUEST)); - } - - if (req->producer_master_addresses_size() <= 0) { - return STATUS( - InvalidArgument, "Producer master address must be provided", req->ShortDebugString(), - MasterError(MasterErrorPB::INVALID_REQUEST)); - } - - if (req->producer_bootstrap_ids().size() > 0 && - req->producer_bootstrap_ids().size() != req->producer_table_ids().size()) { - return STATUS( - InvalidArgument, "Number of bootstrap ids must be equal to number of tables", - req->ShortDebugString(), MasterError(MasterErrorPB::INVALID_REQUEST)); - } - - { - auto l = ClusterConfig()->LockForRead(); - if (l->pb.cluster_uuid() == req->replication_group_id()) { - return STATUS( - InvalidArgument, "The request UUID and cluster UUID are identical.", - req->ShortDebugString(), MasterError(MasterErrorPB::INVALID_REQUEST)); - } - } - - RETURN_NOT_OK_PREPEND( - ValidateMasterAddressesBelongToDifferentCluster(req->producer_master_addresses()), - req->ShortDebugString()); - - SetupReplicationInfo setup_info; - setup_info.transactional = req->transactional(); - auto& table_id_to_bootstrap_id = setup_info.table_bootstrap_ids; - - if (!req->producer_bootstrap_ids().empty()) { - if (req->producer_table_ids().size() != req->producer_bootstrap_ids_size()) { - return STATUS( - InvalidArgument, "Bootstrap ids must be provided for all tables", req->ShortDebugString(), - MasterError(MasterErrorPB::INVALID_REQUEST)); - } - - table_id_to_bootstrap_id.reserve(req->producer_table_ids().size()); - for (int i = 0; i < req->producer_table_ids().size(); i++) { - table_id_to_bootstrap_id.insert_or_assign( - req->producer_table_ids(i), - VERIFY_RESULT(xrepl::StreamId::FromString(req->producer_bootstrap_ids(i)))); - } - } - - SCHECK( - req->producer_namespaces().empty() || req->transactional(), InvalidArgument, - "Transactional flag must be set for Db scoped replication groups"); - - std::vector producer_namespace_ids, consumer_namespace_ids; - for (const auto& producer_ns_id : req->producer_namespaces()) { - SCHECK(!producer_ns_id.id().empty(), InvalidArgument, "Invalid Namespace Id"); - SCHECK(!producer_ns_id.name().empty(), InvalidArgument, "Invalid Namespace name"); - SCHECK_EQ( - producer_ns_id.database_type(), YQLDatabase::YQL_DATABASE_PGSQL, InvalidArgument, - "Invalid Namespace database_type"); - - producer_namespace_ids.push_back(producer_ns_id.id()); - - NamespaceIdentifierPB consumer_ns_id; - consumer_ns_id.set_database_type(YQLDatabase::YQL_DATABASE_PGSQL); - consumer_ns_id.set_name(producer_ns_id.name()); - auto ns_info = VERIFY_RESULT(FindNamespace(consumer_ns_id)); - consumer_namespace_ids.push_back(ns_info->id()); - } - - // We should set the universe uuid even if we fail with AlreadyPresent error. - { - auto universe_uuid = GetUniverseUuidIfExists(); - if (universe_uuid) { - resp->set_universe_uuid(universe_uuid->ToString()); - } - } - - auto ri = VERIFY_RESULT(CreateUniverseReplicationInfoForProducer( - xcluster::ReplicationGroupId(req->replication_group_id()), req->producer_master_addresses(), - producer_namespace_ids, consumer_namespace_ids, req->producer_table_ids(), - setup_info.transactional)); - - // Initialize the CDC Stream by querying the Producer server for RPC sanity checks. - auto result = ri->GetOrCreateXClusterRpcTasks(req->producer_master_addresses()); - if (!result.ok()) { - MarkUniverseReplicationFailed(ri, ResultToStatus(result)); - return SetupError(resp->mutable_error(), MasterErrorPB::INVALID_REQUEST, result.status()); - } - std::shared_ptr xcluster_rpc = *result; - - // For each table, run an async RPC task to verify a sufficient Producer:Consumer schema match. - for (int i = 0; i < req->producer_table_ids_size(); i++) { - // SETUP CONTINUES after this async call. - Status s; - if (IsColocatedDbParentTableId(req->producer_table_ids(i))) { - auto tables_info = std::make_shared>(); - s = xcluster_rpc->client()->GetColocatedTabletSchemaByParentTableId( - req->producer_table_ids(i), tables_info, - Bind( - &CatalogManager::GetColocatedTabletSchemaCallback, Unretained(this), - ri->ReplicationGroupId(), tables_info, setup_info)); - } else if (IsTablegroupParentTableId(req->producer_table_ids(i))) { - auto tablegroup_id = GetTablegroupIdFromParentTableId(req->producer_table_ids(i)); - auto tables_info = std::make_shared>(); - s = xcluster_rpc->client()->GetTablegroupSchemaById( - tablegroup_id, tables_info, - Bind( - &CatalogManager::GetTablegroupSchemaCallback, Unretained(this), - ri->ReplicationGroupId(), tables_info, tablegroup_id, setup_info)); - } else { - auto table_info = std::make_shared(); - s = xcluster_rpc->client()->GetTableSchemaById( - req->producer_table_ids(i), table_info, - Bind( - &CatalogManager::GetTableSchemaCallback, Unretained(this), ri->ReplicationGroupId(), - table_info, setup_info)); - } - - if (!s.ok()) { - MarkUniverseReplicationFailed(ri, s); - return SetupError(resp->mutable_error(), MasterErrorPB::INVALID_REQUEST, s); - } - } - - LOG(INFO) << "Started schema validation for universe replication " << ri->ToString(); - return Status::OK(); -} - -void CatalogManager::MarkUniverseReplicationFailed( - scoped_refptr universe, const Status& failure_status) { - auto l = universe->LockForWrite(); - MarkUniverseReplicationFailed(failure_status, &l, universe); -} - -void CatalogManager::MarkUniverseReplicationFailed( - const Status& failure_status, CowWriteLock* universe_lock, - scoped_refptr universe) { - auto& l = *universe_lock; - if (l->pb.state() == SysUniverseReplicationEntryPB::DELETED) { - l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::DELETED_ERROR); - } else { - l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::FAILED); - } - - LOG(WARNING) << "Universe replication " << universe->ToString() - << " failed: " << failure_status.ToString(); - - universe->SetSetupUniverseReplicationErrorStatus(failure_status); - - // Update sys_catalog. - const Status s = sys_catalog_->Upsert(leader_ready_term(), universe); - - l.CommitOrWarn(s, "updating universe replication info in sys-catalog"); -} - -void CatalogManager::MarkReplicationBootstrapFailed( - scoped_refptr bootstrap_info, - const Status& failure_status) { - auto l = bootstrap_info->LockForWrite(); - MarkReplicationBootstrapFailed(failure_status, &l, bootstrap_info); -} - -void CatalogManager::MarkReplicationBootstrapFailed( - const Status& failure_status, - CowWriteLock* bootstrap_info_lock, - scoped_refptr bootstrap_info) { - auto& l = *bootstrap_info_lock; - auto state = l->pb.state(); - if (state == SysUniverseReplicationBootstrapEntryPB::DELETED) { - l.mutable_data()->pb.set_state(SysUniverseReplicationBootstrapEntryPB::DELETED_ERROR); - } else { - l.mutable_data()->pb.set_state(SysUniverseReplicationBootstrapEntryPB::FAILED); - l.mutable_data()->pb.set_failed_on(state); - } - - LOG(WARNING) << Format( - "Replication bootstrap $0 failed: $1", bootstrap_info->ToString(), - failure_status.ToString()); - - bootstrap_info->SetReplicationBootstrapErrorStatus(failure_status); - - // Update sys_catalog. - const Status s = sys_catalog_->Upsert(leader_ready_term(), bootstrap_info); - - l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); -} - -void CatalogManager::SetReplicationBootstrapState( - scoped_refptr bootstrap_info, - const SysUniverseReplicationBootstrapEntryPB::State& state) { - auto l = bootstrap_info->LockForWrite(); - l.mutable_data()->set_state(state); - - // Update sys_catalog. - const Status s = sys_catalog_->Upsert(leader_ready_term(), bootstrap_info); - l.CommitOrWarn(s, "updating universe replication bootstrap info in sys-catalog"); -} - -Status CatalogManager::IsBootstrapRequiredOnProducer( - scoped_refptr universe, const TableId& producer_table, - const std::unordered_map& table_bootstrap_ids) { - if (!FLAGS_check_bootstrap_required) { - return Status::OK(); - } - auto master_addresses = universe->LockForRead()->pb.producer_master_addresses(); - boost::optional bootstrap_id; - if (table_bootstrap_ids.count(producer_table) > 0) { - bootstrap_id = table_bootstrap_ids.at(producer_table); - } - - auto xcluster_rpc = VERIFY_RESULT(universe->GetOrCreateXClusterRpcTasks(master_addresses)); - if (VERIFY_RESULT(xcluster_rpc->client()->IsBootstrapRequired({producer_table}, bootstrap_id))) { - return STATUS( - IllegalState, - Format( - "Error Missing Data in Logs. Bootstrap is required for producer $0", universe->id())); - } - return Status::OK(); -} - -Status CatalogManager::IsTableBootstrapRequired( - const TableId& table_id, - const xrepl::StreamId& stream_id, - CoarseTimePoint deadline, - bool* const bootstrap_required) { - scoped_refptr table = VERIFY_RESULT(FindTableById(table_id)); - RSTATUS_DCHECK(table != nullptr, NotFound, "Table ID not found: " + table_id); - - // Make a batch call for IsBootstrapRequired on every relevant TServer. - std::map, cdc::IsBootstrapRequiredRequestPB> - proxy_to_request; - for (const auto& tablet : VERIFY_RESULT(table->GetTablets())) { - auto ts = VERIFY_RESULT(tablet->GetLeader()); - std::shared_ptr proxy; - RETURN_NOT_OK(ts->GetProxy(&proxy)); - proxy_to_request[proxy].add_tablet_ids(tablet->id()); - } - - // TODO: Make the RPCs async and parallel. - *bootstrap_required = false; - for (auto& proxy_request : proxy_to_request) { - auto& tablet_req = proxy_request.second; - cdc::IsBootstrapRequiredResponsePB tablet_resp; - rpc::RpcController rpc; - rpc.set_deadline(deadline); - if (stream_id) { - tablet_req.set_stream_id(stream_id.ToString()); - } - auto& cdc_service = proxy_request.first; - - RETURN_NOT_OK(cdc_service->IsBootstrapRequired(tablet_req, &tablet_resp, &rpc)); - if (tablet_resp.has_error()) { - RETURN_NOT_OK(StatusFromPB(tablet_resp.error().status())); - } else if (tablet_resp.has_bootstrap_required() && tablet_resp.bootstrap_required()) { - *bootstrap_required = true; - break; - } - } - - return Status::OK(); -} - -Status CatalogManager::AddValidatedTableToUniverseReplication( - scoped_refptr universe, - const TableId& producer_table, - const TableId& consumer_table, - const SchemaVersion& producer_schema_version, - const SchemaVersion& consumer_schema_version, - const ColocationSchemaVersions& colocated_schema_versions) { - auto l = universe->LockForWrite(); - - auto map = l.mutable_data()->pb.mutable_validated_tables(); - (*map)[producer_table] = consumer_table; - - SchemaVersionMappingEntryPB entry; - if (IsColocationParentTableId(consumer_table)) { - for (const auto& [colocation_id, producer_schema_version, consumer_schema_version] : - colocated_schema_versions) { - auto colocated_entry = entry.add_colocated_schema_versions(); - auto colocation_mapping = colocated_entry->mutable_schema_version_mapping(); - colocated_entry->set_colocation_id(colocation_id); - colocation_mapping->set_producer_schema_version(producer_schema_version); - colocation_mapping->set_consumer_schema_version(consumer_schema_version); - } - } else { - auto mapping = entry.mutable_schema_version_mapping(); - mapping->set_producer_schema_version(producer_schema_version); - mapping->set_consumer_schema_version(consumer_schema_version); - } - - - auto schema_versions_map = l.mutable_data()->pb.mutable_schema_version_mappings(); - (*schema_versions_map)[producer_table] = std::move(entry); - - // TODO: end of config validation should be where SetupUniverseReplication exits back to user - LOG(INFO) << "UpdateItem in AddValidatedTable"; - - // Update sys_catalog. - RETURN_ACTION_NOT_OK( - sys_catalog_->Upsert(leader_ready_term(), universe), - "updating universe replication info in sys-catalog"); - l.Commit(); - - return Status::OK(); -} - -Status CatalogManager::CreateCdcStreamsIfReplicationValidated( - scoped_refptr universe, - const std::unordered_map& table_bootstrap_ids) { - auto l = universe->LockForWrite(); - if (l->is_deleted_or_failed()) { - // Nothing to do since universe is being deleted. - return STATUS(Aborted, "Universe is being deleted"); - } - - auto* mutable_pb = &l.mutable_data()->pb; - - if (mutable_pb->state() != SysUniverseReplicationEntryPB::INITIALIZING) { - VLOG_WITH_FUNC(2) << "Universe replication is in invalid state " << l->pb.state(); - - // Replication stream has already been validated, or is in FAILED state which cannot be - // recovered. - return Status::OK(); - } - - if (mutable_pb->validated_tables_size() != mutable_pb->tables_size()) { - // Replication stream is not yet ready. All the tables have to be validated. - return Status::OK(); - } - - auto master_addresses = mutable_pb->producer_master_addresses(); - cdc::StreamModeTransactional transactional(mutable_pb->transactional()); - auto res = universe->GetOrCreateXClusterRpcTasks(master_addresses); - if (!res.ok()) { - MarkUniverseReplicationFailed(res.status(), &l, universe); - return STATUS( - InternalError, - Format( - "Error while setting up client for producer $0: $1", universe->id(), - res.status().ToString())); - } - std::shared_ptr xcluster_rpc = *res; - - // Now, all tables are validated. - vector validated_tables; - auto& tbl_iter = mutable_pb->tables(); - validated_tables.insert(validated_tables.begin(), tbl_iter.begin(), tbl_iter.end()); - - mutable_pb->set_state(SysUniverseReplicationEntryPB::VALIDATED); - // Update sys_catalog. - RETURN_ACTION_NOT_OK( - sys_catalog_->Upsert(leader_ready_term(), universe), - "updating universe replication info in sys-catalog"); - l.Commit(); - - // Create CDC stream for each validated table, after persisting the replication state change. - if (!validated_tables.empty()) { - // Keep track of the bootstrap_id, table_id, and options of streams to update after - // the last GetCDCStreamCallback finishes. Will be updated by multiple async - // GetCDCStreamCallback. - auto stream_update_infos = std::make_shared(); - stream_update_infos->reserve(validated_tables.size()); - auto update_infos_lock = std::make_shared(); - - for (const auto& table : validated_tables) { - auto producer_bootstrap_id = FindOrNull(table_bootstrap_ids, table); - if (producer_bootstrap_id && *producer_bootstrap_id) { - auto table_id = std::make_shared(); - auto stream_options = std::make_shared>(); - xcluster_rpc->client()->GetCDCStream( - *producer_bootstrap_id, table_id, stream_options, - std::bind( - &CatalogManager::GetCDCStreamCallback, this, *producer_bootstrap_id, table_id, - stream_options, universe->ReplicationGroupId(), table, xcluster_rpc, - std::placeholders::_1, stream_update_infos, update_infos_lock)); - } else { - // Streams are used as soon as they are created so set state to active. - client::XClusterClient(*xcluster_rpc->client()) - .CreateXClusterStreamAsync( - table, /*active=*/true, transactional, - std::bind( - &CatalogManager::AddCDCStreamToUniverseAndInitConsumer, this, - universe->ReplicationGroupId(), table, std::placeholders::_1, - nullptr /* on_success_cb */)); - } - } - } - return Status::OK(); -} - -Status CatalogManager::AddValidatedTableAndCreateCdcStreams( - scoped_refptr universe, - const std::unordered_map& table_bootstrap_ids, - const TableId& producer_table, - const TableId& consumer_table, - const ColocationSchemaVersions& colocated_schema_versions) { - RETURN_NOT_OK(AddValidatedTableToUniverseReplication(universe, producer_table, consumer_table, - cdc::kInvalidSchemaVersion, - cdc::kInvalidSchemaVersion, - colocated_schema_versions)); - return CreateCdcStreamsIfReplicationValidated(universe, table_bootstrap_ids); -} - -void CatalogManager::GetTableSchemaCallback( - const xcluster::ReplicationGroupId& replication_group_id, - const std::shared_ptr& producer_info, - const SetupReplicationInfo& setup_info, const Status& s) { - // First get the universe. - scoped_refptr universe; - { - SharedLock lock(mutex_); - TRACE("Acquired catalog manager lock"); - - universe = FindPtrOrNull(universe_replication_map_, replication_group_id); - if (universe == nullptr) { - LOG(ERROR) << "Universe not found: " << replication_group_id; - return; - } - } - - string action = "getting schema for table"; - auto status = s; - if (status.ok()) { - action = "validating table schema and creating CDC stream"; - status = ValidateTableAndCreateCdcStreams(universe, producer_info, setup_info); - } - - if (!status.ok()) { - LOG(ERROR) << "Error " << action << ". Universe: " << replication_group_id - << ", Table: " << producer_info->table_id << ": " << status; - MarkUniverseReplicationFailed(universe, status); - } -} - -Status CatalogManager::ValidateTableAndCreateCdcStreams( - scoped_refptr universe, - const std::shared_ptr& producer_info, - const SetupReplicationInfo& setup_info) { - auto l = universe->LockForWrite(); - if (producer_info->table_name.namespace_name() == master::kSystemNamespaceName) { - auto status = STATUS(IllegalState, "Cannot replicate system tables."); - MarkUniverseReplicationFailed(status, &l, universe); - return status; - } - RETURN_ACTION_NOT_OK( - sys_catalog_->Upsert(leader_ready_term(), universe), - "updating system tables in universe replication"); - l.Commit(); - - GetTableSchemaResponsePB consumer_schema; - RETURN_NOT_OK(ValidateTableSchemaForXCluster(*producer_info, setup_info, &consumer_schema)); - - // If Bootstrap Id is passed in then it must be provided for all tables. - const auto& producer_bootstrap_ids = setup_info.table_bootstrap_ids; - SCHECK( - producer_bootstrap_ids.empty() || producer_bootstrap_ids.contains(producer_info->table_id), - NotFound, - Format("Bootstrap id not found for table $0", producer_info->table_name.ToString())); - - RETURN_NOT_OK( - IsBootstrapRequiredOnProducer(universe, producer_info->table_id, producer_bootstrap_ids)); - - SchemaVersion producer_schema_version = producer_info->schema.version(); - SchemaVersion consumer_schema_version = consumer_schema.version(); - ColocationSchemaVersions colocated_schema_versions; - RETURN_NOT_OK(AddValidatedTableToUniverseReplication( - universe, producer_info->table_id, consumer_schema.identifier().table_id(), - producer_schema_version, consumer_schema_version, colocated_schema_versions)); - - return CreateCdcStreamsIfReplicationValidated(universe, producer_bootstrap_ids); -} - -void CatalogManager::GetTablegroupSchemaCallback( - const xcluster::ReplicationGroupId& replication_group_id, - const std::shared_ptr>& infos, - const TablegroupId& producer_tablegroup_id, const SetupReplicationInfo& setup_info, - const Status& s) { - // First get the universe. - scoped_refptr universe; - { - SharedLock lock(mutex_); - TRACE("Acquired catalog manager lock"); - - universe = FindPtrOrNull(universe_replication_map_, replication_group_id); - if (universe == nullptr) { - LOG(ERROR) << "Universe not found: " << replication_group_id; - return; - } - } - - auto status = - GetTablegroupSchemaCallbackInternal(universe, *infos, producer_tablegroup_id, setup_info, s); - if (!status.ok()) { - std::ostringstream oss; - for (size_t i = 0; i < infos->size(); ++i) { - oss << ((i == 0) ? "" : ", ") << (*infos)[i].table_id; - } - LOG(ERROR) << "Error processing for tables: [ " << oss.str() - << " ] for xCluster replication group " << replication_group_id << ": " << status; - MarkUniverseReplicationFailed(universe, status); - } -} - -Status CatalogManager::GetTablegroupSchemaCallbackInternal( - scoped_refptr& universe, const std::vector& infos, - const TablegroupId& producer_tablegroup_id, const SetupReplicationInfo& setup_info, - const Status& s) { - RETURN_NOT_OK(s); - - SCHECK(!infos.empty(), IllegalState, Format("Tablegroup $0 is empty", producer_tablegroup_id)); - - // validated_consumer_tables contains the table IDs corresponding to that - // from the producer tables. - std::unordered_set validated_consumer_tables; - ColocationSchemaVersions colocated_schema_versions; - colocated_schema_versions.reserve(infos.size()); - for (const auto& info : infos) { - // Validate each of the member table in the tablegroup. - GetTableSchemaResponsePB resp; - RETURN_NOT_OK(ValidateTableSchemaForXCluster(info, setup_info, &resp)); - - colocated_schema_versions.emplace_back( - resp.schema().colocated_table_id().colocation_id(), info.schema.version(), resp.version()); - validated_consumer_tables.insert(resp.identifier().table_id()); - } - - // Get the consumer tablegroup ID. Since this call is expensive (one needs to reverse lookup - // the tablegroup ID from table ID), we only do this call once and do validation afterward. - TablegroupId consumer_tablegroup_id; - // Starting Colocation GA, colocated databases create implicit underlying tablegroups. - bool colocated_database; - { - SharedLock lock(mutex_); - const auto* tablegroup = tablegroup_manager_->FindByTable(*validated_consumer_tables.begin()); - SCHECK( - tablegroup, IllegalState, - Format("No consumer tablegroup found for producer tablegroup: $0", producer_tablegroup_id)); - - consumer_tablegroup_id = tablegroup->id(); - - auto ns = FindPtrOrNull(namespace_ids_map_, tablegroup->database_id()); - SCHECK( - ns, IllegalState, - Format("Could not find namespace by namespace id $0", tablegroup->database_id())); - colocated_database = ns->colocated(); - } - - // tables_in_consumer_tablegroup are the tables listed within the consumer_tablegroup_id. - // We need validated_consumer_tables and tables_in_consumer_tablegroup to be identical. - std::unordered_set tables_in_consumer_tablegroup; - { - GetTablegroupSchemaRequestPB req; - GetTablegroupSchemaResponsePB resp; - req.mutable_tablegroup()->set_id(consumer_tablegroup_id); - auto status = GetTablegroupSchema(&req, &resp); - if (status.ok() && resp.has_error()) { - status = StatusFromPB(resp.error().status()); - } - RETURN_NOT_OK_PREPEND( - status, - Format("Error when getting consumer tablegroup schema: $0", consumer_tablegroup_id)); - - for (const auto& info : resp.get_table_schema_response_pbs()) { - tables_in_consumer_tablegroup.insert(info.identifier().table_id()); - } - } - - if (validated_consumer_tables != tables_in_consumer_tablegroup) { - return STATUS( - IllegalState, - Format( - "Mismatch between tables associated with producer tablegroup $0 and " - "tables in consumer tablegroup $1: ($2) vs ($3).", - producer_tablegroup_id, consumer_tablegroup_id, AsString(validated_consumer_tables), - AsString(tables_in_consumer_tablegroup))); - } - - RETURN_NOT_OK_PREPEND( - IsBootstrapRequiredOnProducer( - universe, producer_tablegroup_id, setup_info.table_bootstrap_ids), - Format( - "Found error while checking if bootstrap is required for table $0", - producer_tablegroup_id)); - - TableId producer_parent_table_id; - TableId consumer_parent_table_id; - if (colocated_database) { - producer_parent_table_id = GetColocationParentTableId(producer_tablegroup_id); - consumer_parent_table_id = GetColocationParentTableId(consumer_tablegroup_id); - } else { - producer_parent_table_id = GetTablegroupParentTableId(producer_tablegroup_id); - consumer_parent_table_id = GetTablegroupParentTableId(consumer_tablegroup_id); - } - - { - SharedLock lock(mutex_); - SCHECK( - !xcluster_manager_->IsTableReplicationConsumer(consumer_parent_table_id), IllegalState, - "N:1 replication topology not supported"); - } - - RETURN_NOT_OK(AddValidatedTableAndCreateCdcStreams( - universe, setup_info.table_bootstrap_ids, producer_parent_table_id, consumer_parent_table_id, - colocated_schema_versions)); - return Status::OK(); -} - -void CatalogManager::GetColocatedTabletSchemaCallback( - const xcluster::ReplicationGroupId& replication_group_id, - const std::shared_ptr>& infos, - const SetupReplicationInfo& setup_info, const Status& s) { - // First get the universe. - scoped_refptr universe; - { - SharedLock lock(mutex_); - TRACE("Acquired catalog manager lock"); - - universe = FindPtrOrNull(universe_replication_map_, replication_group_id); - if (universe == nullptr) { - LOG(ERROR) << "Universe not found: " << replication_group_id; - return; - } - } - - if (!s.ok()) { - MarkUniverseReplicationFailed(universe, s); - std::ostringstream oss; - for (size_t i = 0; i < infos->size(); ++i) { - oss << ((i == 0) ? "" : ", ") << (*infos)[i].table_id; - } - LOG(ERROR) << "Error getting schema for tables: [ " << oss.str() << " ]: " << s; - return; - } - - if (infos->empty()) { - LOG(WARNING) << "Received empty list of tables to validate: " << s; - return; - } - - // Validate table schemas. - std::unordered_set producer_parent_table_ids; - std::unordered_set consumer_parent_table_ids; - ColocationSchemaVersions colocated_schema_versions; - colocated_schema_versions.reserve(infos->size()); - for (const auto& info : *infos) { - // Verify that we have a colocated table. - if (!info.colocated) { - MarkUniverseReplicationFailed( - universe, - STATUS(InvalidArgument, Format("Received non-colocated table: $0", info.table_id))); - LOG(ERROR) << "Received non-colocated table: " << info.table_id; - return; - } - // Validate each table, and get the parent colocated table id for the consumer. - GetTableSchemaResponsePB resp; - Status table_status = ValidateTableSchemaForXCluster(info, setup_info, &resp); - if (!table_status.ok()) { - MarkUniverseReplicationFailed(universe, table_status); - LOG(ERROR) << "Found error while validating table schema for table " << info.table_id << ": " - << table_status; - return; - } - // Store the parent table ids. - producer_parent_table_ids.insert(GetColocatedDbParentTableId(info.table_name.namespace_id())); - consumer_parent_table_ids.insert( - GetColocatedDbParentTableId(resp.identifier().namespace_().id())); - colocated_schema_versions.emplace_back( - resp.schema().colocated_table_id().colocation_id(), info.schema.version(), resp.version()); - } - - // Verify that we only found one producer and one consumer colocated parent table id. - if (producer_parent_table_ids.size() != 1) { - auto message = Format( - "Found incorrect number of producer colocated parent table ids. " - "Expected 1, but found: $0", - AsString(producer_parent_table_ids)); - MarkUniverseReplicationFailed(universe, STATUS(InvalidArgument, message)); - LOG(ERROR) << message; - return; - } - if (consumer_parent_table_ids.size() != 1) { - auto message = Format( - "Found incorrect number of consumer colocated parent table ids. " - "Expected 1, but found: $0", - AsString(consumer_parent_table_ids)); - MarkUniverseReplicationFailed(universe, STATUS(InvalidArgument, message)); - LOG(ERROR) << message; - return; - } - - { - SharedLock lock(mutex_); - if (xcluster_manager_->IsTableReplicationConsumer(*consumer_parent_table_ids.begin())) { - std::string message = "N:1 replication topology not supported"; - MarkUniverseReplicationFailed(universe, STATUS(IllegalState, message)); - LOG(ERROR) << message; - return; - } - } - - Status status = IsBootstrapRequiredOnProducer( - universe, *producer_parent_table_ids.begin(), setup_info.table_bootstrap_ids); - if (!status.ok()) { - MarkUniverseReplicationFailed(universe, status); - LOG(ERROR) << "Found error while checking if bootstrap is required for table " - << *producer_parent_table_ids.begin() << ": " << status; - } - - status = AddValidatedTableAndCreateCdcStreams( - universe, - setup_info.table_bootstrap_ids, - *producer_parent_table_ids.begin(), - *consumer_parent_table_ids.begin(), - colocated_schema_versions); - - if (!status.ok()) { - LOG(ERROR) << "Found error while adding validated table to system catalog: " - << *producer_parent_table_ids.begin() << ": " << status; - return; - } -} - -void CatalogManager::GetCDCStreamCallback( - const xrepl::StreamId& bootstrap_id, std::shared_ptr table_id, - std::shared_ptr> options, - const xcluster::ReplicationGroupId& replication_group_id, const TableId& table, - std::shared_ptr xcluster_rpc, const Status& s, - std::shared_ptr stream_update_infos, - std::shared_ptr update_infos_lock) { - if (!s.ok()) { - LOG(ERROR) << "Unable to find bootstrap id " << bootstrap_id; - AddCDCStreamToUniverseAndInitConsumer(replication_group_id, table, s); - return; - } - - if (*table_id != table) { - const Status invalid_bootstrap_id_status = STATUS_FORMAT( - InvalidArgument, "Invalid bootstrap id for table $0. Bootstrap id $1 belongs to table $2", - table, bootstrap_id, *table_id); - LOG(ERROR) << invalid_bootstrap_id_status; - AddCDCStreamToUniverseAndInitConsumer(replication_group_id, table, invalid_bootstrap_id_status); - return; - } - - scoped_refptr original_universe; - { - SharedLock lock(mutex_); - original_universe = FindPtrOrNull( - universe_replication_map_, xcluster::GetOriginalReplicationGroupId(replication_group_id)); - } - - if (original_universe == nullptr) { - LOG(ERROR) << "Universe not found: " << replication_group_id; - return; - } - - cdc::StreamModeTransactional transactional(original_universe->LockForRead()->pb.transactional()); - - // todo check options - { - std::lock_guard lock(*update_infos_lock); - stream_update_infos->push_back({bootstrap_id, *table_id, *options}); - } - - const auto update_xrepl_stream_func = [&]() -> Status { - // Extra callback on universe setup success - update the producer to let it know that - // the bootstrapping is complete. This callback will only be called once among all - // the GetCDCStreamCallback calls, and we update all streams in batch at once. - - std::vector update_bootstrap_ids; - std::vector update_entries; - { - std::lock_guard lock(*update_infos_lock); - - for (const auto& [update_bootstrap_id, update_table_id, update_options] : - *stream_update_infos) { - SysCDCStreamEntryPB new_entry; - new_entry.add_table_id(update_table_id); - new_entry.mutable_options()->Reserve(narrow_cast(update_options.size())); - for (const auto& [key, value] : update_options) { - if (key == cdc::kStreamState) { - // We will set state explicitly. - continue; - } - auto new_option = new_entry.add_options(); - new_option->set_key(key); - new_option->set_value(value); - } - new_entry.set_state(master::SysCDCStreamEntryPB::ACTIVE); - new_entry.set_transactional(transactional); - - update_bootstrap_ids.push_back(update_bootstrap_id); - update_entries.push_back(new_entry); - } - } - - RETURN_NOT_OK_PREPEND( - xcluster_rpc->client()->UpdateCDCStream(update_bootstrap_ids, update_entries), - "Unable to update xrepl stream options on source universe"); - - { - std::lock_guard lock(*update_infos_lock); - stream_update_infos->clear(); - } - return Status::OK(); - }; - - AddCDCStreamToUniverseAndInitConsumer( - replication_group_id, table, bootstrap_id, update_xrepl_stream_func); -} - -void CatalogManager::AddCDCStreamToUniverseAndInitConsumer( - const xcluster::ReplicationGroupId& replication_group_id, const TableId& table_id, - const Result& stream_id, std::function on_success_cb) { - scoped_refptr universe; - { - SharedLock lock(mutex_); - TRACE("Acquired catalog manager lock"); - - universe = FindPtrOrNull(universe_replication_map_, replication_group_id); - if (universe == nullptr) { - LOG(ERROR) << "Universe not found: " << replication_group_id; - return; - } - } - - Status s; - if (!stream_id.ok()) { - s = std::move(stream_id).status(); - } else { - s = AddCDCStreamToUniverseAndInitConsumerInternal( - universe, table_id, *stream_id, std::move(on_success_cb)); - } - - if (!s.ok()) { - MarkUniverseReplicationFailed(universe, s); - } -} - -Status CatalogManager::AddCDCStreamToUniverseAndInitConsumerInternal( - scoped_refptr universe, const TableId& table_id, - const xrepl::StreamId& stream_id, std::function on_success_cb) { - bool merge_alter = false; - bool validated_all_tables = false; - std::vector consumer_info; - { - auto l = universe->LockForWrite(); - if (l->is_deleted_or_failed()) { - // Nothing to do if universe is being deleted. - return Status::OK(); - } - - auto map = l.mutable_data()->pb.mutable_table_streams(); - (*map)[table_id] = stream_id.ToString(); - - // This functions as a barrier: waiting for the last RPC call from GetTableSchemaCallback. - if (l.mutable_data()->pb.table_streams_size() == l->pb.tables_size()) { - // All tables successfully validated! Register CDC consumers & start replication. - validated_all_tables = true; - LOG(INFO) << "Registering CDC consumers for universe " << universe->id(); - - consumer_info.reserve(l->pb.tables_size()); - std::set consumer_table_ids; - for (const auto& [producer_table_id, consumer_table_id] : l->pb.validated_tables()) { - consumer_table_ids.insert(consumer_table_id); - - XClusterConsumerStreamInfo info; - info.producer_table_id = producer_table_id; - info.consumer_table_id = consumer_table_id; - info.stream_id = VERIFY_RESULT(xrepl::StreamId::FromString((*map)[producer_table_id])); - consumer_info.push_back(info); - } - - if (l->IsDbScoped()) { - std::vector consumer_namespace_ids; - for (const auto& ns_info : l->pb.db_scoped_info().namespace_infos()) { - consumer_namespace_ids.push_back(ns_info.consumer_namespace_id()); - } - RETURN_NOT_OK(ValidateTableListForDbScopedReplication( - *universe, consumer_namespace_ids, consumer_table_ids, *this)); - } - - std::vector hp; - HostPortsFromPBs(l->pb.producer_master_addresses(), &hp); - auto xcluster_rpc_tasks = - VERIFY_RESULT(universe->GetOrCreateXClusterRpcTasks(l->pb.producer_master_addresses())); - RETURN_NOT_OK(InitXClusterConsumer( - consumer_info, HostPort::ToCommaSeparatedString(hp), *universe.get(), - xcluster_rpc_tasks)); - - if (xcluster::IsAlterReplicationGroupId(universe->ReplicationGroupId())) { - // Don't enable ALTER universes, merge them into the main universe instead. - // on_success_cb will be invoked in MergeUniverseReplication. - merge_alter = true; - } else { - l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::ACTIVE); - if (on_success_cb) { - // Before updating, run any callbacks on success. - RETURN_NOT_OK(on_success_cb()); - } - } - } - - // Update sys_catalog with new producer table id info. - RETURN_NOT_OK(sys_catalog_->Upsert(leader_ready_term(), universe)); - - l.Commit(); - } - - if (!validated_all_tables) { - return Status::OK(); - } - - auto final_id = xcluster::GetOriginalReplicationGroupId(universe->ReplicationGroupId()); - // If this is an 'alter', merge back into primary command now that setup is a success. - if (merge_alter) { - RETURN_NOT_OK(MergeUniverseReplication(universe, final_id, std::move(on_success_cb))); - } - // Update the in-memory cache of consumer tables. - for (const auto& info : consumer_info) { - xcluster_manager_->RecordTableConsumerStream(info.consumer_table_id, final_id, info.stream_id); - } - - return Status::OK(); -} - - -Status CatalogManager::UpdateCDCProducerOnTabletSplit( - const TableId& producer_table_id, const SplitTabletIds& split_tablet_ids) { - std::vector streams; - std::vector entries; - for (const auto stream_type : {cdc::XCLUSTER, cdc::CDCSDK}) { - if (stream_type == cdc::CDCSDK && - FLAGS_cdcsdk_enable_cleanup_of_non_eligible_tables_from_stream) { - const auto& table_info = GetTableInfo(producer_table_id); - // Skip adding children tablet entries in cdc state if the table is an index or a mat view. - // These tables, if present in CDC stream, are anyway going to be removed by a bg thread. This - // check ensures even if there is a race condition where a tablet of a non-eligible table - // splits and concurrently we are removing such tables from stream, the child tables do not - // get added. - { - SharedLock lock(mutex_); - if (!IsTableEligibleForCDCSDKStream(table_info, std::nullopt)) { - LOG(INFO) << "Skipping adding children tablets to cdc state for table " - << producer_table_id << " as it is not meant to part of a CDC stream"; - continue; - } - } - } - - { - SharedLock lock(mutex_); - streams = GetXReplStreamsForTable(producer_table_id, stream_type); - } - - for (const auto& stream : streams) { - auto last_active_time = GetCurrentTimeMicros(); - - std::optional parent_entry_opt; - if (stream_type == cdc::CDCSDK) { - parent_entry_opt = VERIFY_RESULT(cdc_state_table_->TryFetchEntry( - {split_tablet_ids.source, stream->StreamId()}, - cdc::CDCStateTableEntrySelector().IncludeActiveTime().IncludeCDCSDKSafeTime())); - DCHECK(parent_entry_opt); - } - - // In the case of a Consistent Snapshot Stream, set the active_time of the children tablets - // to the corresponding value in the parent tablet. - // This will allow to establish that a child tablet is of interest to a stream - // iff the parent tablet is also of interest to the stream. - // Thus, retention barriers, inherited from the parent tablet, can be released - // on the children tablets also if not of interest to the stream - if (stream->IsConsistentSnapshotStream()) { - LOG_WITH_FUNC(INFO) << "Copy active time from parent to child tablets" - << " Tablets involved: " << split_tablet_ids.ToString() - << " Consistent Snapshot StreamId: " << stream->StreamId(); - DCHECK(parent_entry_opt->active_time); - if (parent_entry_opt && parent_entry_opt->active_time) { - last_active_time = *parent_entry_opt->active_time; - } else { - LOG_WITH_FUNC(WARNING) - << Format("Did not find $0 value in the cdc state table", - parent_entry_opt ? "active_time" : "row") - << " for parent tablet: " << split_tablet_ids.source - << " and stream: " << stream->StreamId(); - } - } - - // Insert children entries into cdc_state now, set the opid to 0.0 and the timestamp to - // NULL. When we process the parent's SPLIT_OP in GetChanges, we will update the opid to - // the SPLIT_OP so that the children pollers continue from the next records. When we process - // the first GetChanges for the children, then their timestamp value will be set. We use - // this information to know that the children has been polled for. Once both children have - // been polled for, then we can delete the parent tablet via the bg task - // DoProcessXClusterParentTabletDeletion. - for (const auto& child_tablet_id : - {split_tablet_ids.children.first, split_tablet_ids.children.second}) { - cdc::CDCStateTableEntry entry(child_tablet_id, stream->StreamId()); - entry.checkpoint = OpId().Min(); - - if (stream_type == cdc::CDCSDK) { - entry.active_time = last_active_time; - DCHECK(parent_entry_opt->cdc_sdk_safe_time); - if (parent_entry_opt && parent_entry_opt->cdc_sdk_safe_time) { - entry.cdc_sdk_safe_time = *parent_entry_opt->cdc_sdk_safe_time; - } else { - LOG_WITH_FUNC(WARNING) << Format( - "Did not find $0 value in the cdc state table", - parent_entry_opt ? "cdc_sdk_safe_time" : "row") - << " for parent tablet: " << split_tablet_ids.source - << " and stream: " << stream->StreamId(); - entry.cdc_sdk_safe_time = last_active_time; - } - } - - entries.push_back(std::move(entry)); - } - } - } - - return cdc_state_table_->InsertEntries(entries); -} - -Status CatalogManager::InitXClusterConsumer( - const std::vector& consumer_info, const std::string& master_addrs, - UniverseReplicationInfo& replication_info, - std::shared_ptr xcluster_rpc_tasks) { - auto universe_l = replication_info.LockForRead(); - auto schema_version_mappings = universe_l->pb.schema_version_mappings(); - - // Get the tablets in the consumer table. - cdc::ProducerEntryPB producer_entry; - - if (FLAGS_enable_xcluster_auto_flag_validation) { - auto compatible_auto_flag_config_version = VERIFY_RESULT( - GetAutoFlagConfigVersionIfCompatible(replication_info, master_->GetAutoFlagsConfig())); - producer_entry.set_compatible_auto_flag_config_version(compatible_auto_flag_config_version); - producer_entry.set_validated_auto_flags_config_version(compatible_auto_flag_config_version); - } - - auto cluster_config = ClusterConfig(); - auto l = cluster_config->LockForWrite(); - auto* consumer_registry = l.mutable_data()->pb.mutable_consumer_registry(); - auto transactional = universe_l->pb.transactional(); - if (!xcluster::IsAlterReplicationGroupId(replication_info.ReplicationGroupId())) { - if (universe_l->IsDbScoped()) { - DCHECK(transactional); - } - } - - for (const auto& stream_info : consumer_info) { - auto consumer_tablet_keys = VERIFY_RESULT(GetTableKeyRanges(stream_info.consumer_table_id)); - auto schema_version = VERIFY_RESULT(GetTableSchemaVersion(stream_info.consumer_table_id)); - - cdc::StreamEntryPB stream_entry; - // Get producer tablets and map them to the consumer tablets - RETURN_NOT_OK(InitXClusterStream( - stream_info.producer_table_id, stream_info.consumer_table_id, consumer_tablet_keys, - &stream_entry, xcluster_rpc_tasks)); - // Set the validated consumer schema version - auto* producer_schema_pb = stream_entry.mutable_producer_schema(); - producer_schema_pb->set_last_compatible_consumer_schema_version(schema_version); - auto* schema_versions = stream_entry.mutable_schema_versions(); - auto mapping = FindOrNull(schema_version_mappings, stream_info.producer_table_id); - SCHECK(mapping, NotFound, Format("No schema mapping for $0", stream_info.producer_table_id)); - if (IsColocationParentTableId(stream_info.consumer_table_id)) { - // Get all the child tables and add their mappings - auto& colocated_schema_versions_pb = *stream_entry.mutable_colocated_schema_versions(); - for (const auto& colocated_entry : mapping->colocated_schema_versions()) { - auto colocation_id = colocated_entry.colocation_id(); - colocated_schema_versions_pb[colocation_id].set_current_producer_schema_version( - colocated_entry.schema_version_mapping().producer_schema_version()); - colocated_schema_versions_pb[colocation_id].set_current_consumer_schema_version( - colocated_entry.schema_version_mapping().consumer_schema_version()); - } - } else { - schema_versions->set_current_producer_schema_version( - mapping->schema_version_mapping().producer_schema_version()); - schema_versions->set_current_consumer_schema_version( - mapping->schema_version_mapping().consumer_schema_version()); - } - - // Mark this stream as special if it is for the ddl_queue table. - auto table_info = GetTableInfo(stream_info.consumer_table_id); - stream_entry.set_is_ddl_queue_table( - table_info->GetTableType() == PGSQL_TABLE_TYPE && - table_info->name() == xcluster::kDDLQueueTableName && - table_info->pgschema_name() == xcluster::kDDLQueuePgSchemaName); - - (*producer_entry.mutable_stream_map())[stream_info.stream_id.ToString()] = - std::move(stream_entry); - } - - // Log the Network topology of the Producer Cluster - auto master_addrs_list = StringSplit(master_addrs, ','); - producer_entry.mutable_master_addrs()->Reserve(narrow_cast(master_addrs_list.size())); - for (const auto& addr : master_addrs_list) { - auto hp = VERIFY_RESULT(HostPort::FromString(addr, 0)); - HostPortToPB(hp, producer_entry.add_master_addrs()); - } - - auto* replication_group_map = consumer_registry->mutable_producer_map(); - SCHECK_EQ( - replication_group_map->count(replication_info.id()), 0, InvalidArgument, - "Already created a consumer for this universe"); - - // TServers will use the ClusterConfig to create CDC Consumers for applicable local tablets. - (*replication_group_map)[replication_info.id()] = std::move(producer_entry); - - l.mutable_data()->pb.set_version(l.mutable_data()->pb.version() + 1); - RETURN_NOT_OK(CheckStatus( - sys_catalog_->Upsert(leader_ready_term(), cluster_config.get()), - "updating cluster config in sys-catalog")); - - xcluster_manager_->SyncConsumerReplicationStatusMap( - replication_info.ReplicationGroupId(), *replication_group_map); - l.Commit(); - - xcluster_manager_->CreateXClusterSafeTimeTableAndStartService(); - - return Status::OK(); -} - -Status CatalogManager::MergeUniverseReplication( - scoped_refptr universe, xcluster::ReplicationGroupId original_id, - std::function on_success_cb) { - // Merge back into primary command now that setup is a success. - LOG(INFO) << "Merging CDC universe: " << universe->id() << " into " << original_id; - - SCHECK( - !FLAGS_TEST_fail_universe_replication_merge, IllegalState, - "TEST_fail_universe_replication_merge"); - - scoped_refptr original_universe; - { - SharedLock lock(mutex_); - TRACE("Acquired catalog manager lock"); - - original_universe = FindPtrOrNull(universe_replication_map_, original_id); - if (original_universe == nullptr) { - LOG(ERROR) << "Universe not found: " << original_id; - return Status::OK(); - } - } - - { - auto cluster_config = ClusterConfig(); - // Acquire Locks in order of Original Universe, Cluster Config, New Universe - auto original_lock = original_universe->LockForWrite(); - auto alter_lock = universe->LockForWrite(); - auto cl = cluster_config->LockForWrite(); - - // Merge Cluster Config for TServers. - auto* consumer_registry = cl.mutable_data()->pb.mutable_consumer_registry(); - auto pm = consumer_registry->mutable_producer_map(); - auto original_producer_entry = pm->find(original_universe->id()); - auto alter_producer_entry = pm->find(universe->id()); - if (original_producer_entry != pm->end() && alter_producer_entry != pm->end()) { - // Merge the Tables from the Alter into the original. - auto as = alter_producer_entry->second.stream_map(); - original_producer_entry->second.mutable_stream_map()->insert(as.begin(), as.end()); - // Delete the Alter - pm->erase(alter_producer_entry); - } else { - LOG(WARNING) << "Could not find both universes in Cluster Config: " << universe->id(); - } - cl.mutable_data()->pb.set_version(cl.mutable_data()->pb.version() + 1); - - // Merge Master Config on Consumer. (no need for Producer changes, since it uses stream_id) - // Merge Table->StreamID mapping. - auto& alter_pb = alter_lock.mutable_data()->pb; - auto& original_pb = original_lock.mutable_data()->pb; - - auto* alter_tables = alter_pb.mutable_tables(); - original_pb.mutable_tables()->MergeFrom(*alter_tables); - alter_tables->Clear(); - auto* alter_table_streams = alter_pb.mutable_table_streams(); - original_pb.mutable_table_streams()->insert( - alter_table_streams->begin(), alter_table_streams->end()); - alter_table_streams->clear(); - auto* alter_validated_tables = alter_pb.mutable_validated_tables(); - original_pb.mutable_validated_tables()->insert( - alter_validated_tables->begin(), alter_validated_tables->end()); - alter_validated_tables->clear(); - if (alter_lock.mutable_data()->IsDbScoped()) { - auto* alter_namespace_info = alter_pb.mutable_db_scoped_info()->mutable_namespace_infos(); - original_pb.mutable_db_scoped_info()->mutable_namespace_infos()->MergeFrom( - *alter_namespace_info); - alter_namespace_info->Clear(); - } - - alter_pb.set_state(SysUniverseReplicationEntryPB::DELETED); - - if (PREDICT_FALSE(FLAGS_TEST_exit_unfinished_merging)) { - return Status::OK(); - } - - if (on_success_cb) { - RETURN_NOT_OK(on_success_cb()); - } - - { - // Need both these updates to be atomic. - auto w = sys_catalog_->NewWriter(leader_ready_term()); - auto s = w->Mutate( - QLWriteRequestPB::QL_STMT_UPDATE, - original_universe.get(), - universe.get(), - cluster_config.get()); - s = CheckStatus( - sys_catalog_->SyncWrite(w.get()), - "Updating universe replication entries and cluster config in sys-catalog"); - } - - xcluster_manager_->SyncConsumerReplicationStatusMap( - original_universe->ReplicationGroupId(), *pm); - xcluster_manager_->SyncConsumerReplicationStatusMap(universe->ReplicationGroupId(), *pm); - - alter_lock.Commit(); - cl.Commit(); - original_lock.Commit(); - } - - // Add alter temp universe to GC. - MarkUniverseForCleanup(universe->ReplicationGroupId()); - - LOG(INFO) << "Done with Merging " << universe->id() << " into " << original_universe->id(); - - xcluster_manager_->CreateXClusterSafeTimeTableAndStartService(); - - return Status::OK(); -} - -Status CatalogManager::DeleteUniverseReplication( - const xcluster::ReplicationGroupId& replication_group_id, bool ignore_errors, - bool skip_producer_stream_deletion, DeleteUniverseReplicationResponsePB* resp) { - scoped_refptr ri; - { - SharedLock lock(mutex_); - TRACE("Acquired catalog manager lock"); - - ri = FindPtrOrNull(universe_replication_map_, replication_group_id); - if (ri == nullptr) { - return STATUS( - NotFound, "Universe replication info does not exist", replication_group_id, - MasterError(MasterErrorPB::OBJECT_NOT_FOUND)); - } - } - - { - auto l = ri->LockForWrite(); - l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::DELETING); - Status s = sys_catalog_->Upsert(leader_ready_term(), ri); - RETURN_NOT_OK( - CheckLeaderStatus(s, "Updating delete universe replication info into sys-catalog")); - TRACE("Wrote universe replication info to sys-catalog"); - l.Commit(); - } - - auto l = ri->LockForWrite(); - l.mutable_data()->pb.set_state(SysUniverseReplicationEntryPB::DELETED); - - // We can skip the deletion of individual streams for DB Scoped replication since deletion of the - // outbound replication group will clean it up. - if (l->IsDbScoped()) { - skip_producer_stream_deletion = true; - } - - // Delete subscribers on the Consumer Registry (removes from TServers). - LOG(INFO) << "Deleting subscribers for producer " << replication_group_id; - { - auto cluster_config = ClusterConfig(); - auto cl = cluster_config->LockForWrite(); - auto* consumer_registry = cl.mutable_data()->pb.mutable_consumer_registry(); - auto replication_group_map = consumer_registry->mutable_producer_map(); - auto it = replication_group_map->find(replication_group_id.ToString()); - if (it != replication_group_map->end()) { - replication_group_map->erase(it); - cl.mutable_data()->pb.set_version(cl.mutable_data()->pb.version() + 1); - RETURN_NOT_OK(CheckStatus( - sys_catalog_->Upsert(leader_ready_term(), cluster_config.get()), - "updating cluster config in sys-catalog")); - - xcluster_manager_->SyncConsumerReplicationStatusMap( - replication_group_id, *replication_group_map); - cl.Commit(); - } - } - - // Delete CDC stream config on the Producer. - if (!l->pb.table_streams().empty() && !skip_producer_stream_deletion) { - auto result = ri->GetOrCreateXClusterRpcTasks(l->pb.producer_master_addresses()); - if (!result.ok()) { - LOG(WARNING) << "Unable to create cdc rpc task. CDC streams won't be deleted: " << result; - } else { - auto xcluster_rpc = *result; - vector streams; - std::unordered_map stream_to_producer_table_id; - for (const auto& [table_id, stream_id_str] : l->pb.table_streams()) { - auto stream_id = VERIFY_RESULT(xrepl::StreamId::FromString(stream_id_str)); - streams.emplace_back(stream_id); - stream_to_producer_table_id.emplace(stream_id, table_id); - } - - DeleteCDCStreamResponsePB delete_cdc_stream_resp; - // Set force_delete=true since we are deleting active xCluster streams. - // Since we are deleting universe replication, we should be ok with - // streams not existing on the other side, so we pass in ignore_errors - bool ignore_missing_streams = false; - auto s = xcluster_rpc->client()->DeleteCDCStream( - streams, - true, /* force_delete */ - true /* ignore_errors */, - &delete_cdc_stream_resp); - - if (delete_cdc_stream_resp.not_found_stream_ids().size() > 0) { - std::vector missing_streams; - missing_streams.reserve(delete_cdc_stream_resp.not_found_stream_ids().size()); - for (const auto& stream_id : delete_cdc_stream_resp.not_found_stream_ids()) { - missing_streams.emplace_back(Format( - "$0 (table_id: $1)", stream_id, - stream_to_producer_table_id[VERIFY_RESULT(xrepl::StreamId::FromString(stream_id))])); - } - auto message = - Format("Could not find the following streams: $0.", AsString(missing_streams)); - - if (s.ok()) { - // Returned but did not find some streams, so still need to warn the user about those. - ignore_missing_streams = true; - s = STATUS(NotFound, message); - } else { - s = s.CloneAndPrepend(message); - } - } - RETURN_NOT_OK(ReturnErrorOrAddWarning(s, ignore_errors | ignore_missing_streams, resp)); - } - } - - if (PREDICT_FALSE(FLAGS_TEST_exit_unfinished_deleting)) { - // Exit for texting services - return Status::OK(); - } - - // Delete universe in the Universe Config. - RETURN_NOT_OK( - ReturnErrorOrAddWarning(DeleteUniverseReplicationUnlocked(ri), ignore_errors, resp)); - l.Commit(); - LOG(INFO) << "Processed delete universe replication of " << ri->ToString(); - - // Run the safe time task as it may need to perform cleanups of it own - xcluster_manager_->CreateXClusterSafeTimeTableAndStartService(); - - return Status::OK(); -} - -Status CatalogManager::DeleteUniverseReplication( - const DeleteUniverseReplicationRequestPB* req, - DeleteUniverseReplicationResponsePB* resp, - rpc::RpcContext* rpc) { - LOG(INFO) << "Servicing DeleteUniverseReplication request from " << RequestorString(rpc) << ": " - << req->ShortDebugString(); - - if (!req->has_replication_group_id()) { - return STATUS( - InvalidArgument, "Producer universe ID required", req->ShortDebugString(), - MasterError(MasterErrorPB::INVALID_REQUEST)); - } - - RETURN_NOT_OK(ValidateUniverseUUID(req, *this)); - - RETURN_NOT_OK(DeleteUniverseReplication( - xcluster::ReplicationGroupId(req->replication_group_id()), req->ignore_errors(), - req->skip_producer_stream_deletion(), resp)); - LOG(INFO) << "Successfully completed DeleteUniverseReplication request from " - << RequestorString(rpc); - return Status::OK(); -} - -Status CatalogManager::DeleteUniverseReplicationUnlocked( - scoped_refptr universe) { - // Assumes that caller has locked universe. - RETURN_ACTION_NOT_OK( - sys_catalog_->Delete(leader_ready_term(), universe), - Format("updating sys-catalog, replication_group_id: $0", universe->id())); - - // Remove it from the map. - LockGuard lock(mutex_); - if (universe_replication_map_.erase(universe->ReplicationGroupId()) < 1) { - LOG(WARNING) << "Failed to remove replication info from map: replication_group_id: " - << universe->id(); - } - - // Also update the mapping of consumer tables. - for (const auto& table : universe->metadata().state().pb.validated_tables()) { - xcluster_manager_->RemoveTableConsumerStream(table.second, universe->ReplicationGroupId()); - } - return Status::OK(); -} - -Status CatalogManager::ChangeXClusterRole( - const ChangeXClusterRoleRequestPB* req, - ChangeXClusterRoleResponsePB* resp, - rpc::RpcContext* rpc) { - LOG(INFO) << "Servicing ChangeXClusterRole request from " << RequestorString(rpc) << ": " - << req->ShortDebugString(); - return Status::OK(); -} - -Status CatalogManager::BootstrapProducer( - const BootstrapProducerRequestPB* req, - BootstrapProducerResponsePB* resp, - rpc::RpcContext* rpc) { - LOG(INFO) << "Servicing BootstrapProducer request from " << RequestorString(rpc) << ": " - << req->ShortDebugString(); - - const bool pg_database_type = req->db_type() == YQL_DATABASE_PGSQL; - SCHECK( - pg_database_type || req->db_type() == YQL_DATABASE_CQL, InvalidArgument, - "Invalid database type"); - SCHECK( - req->has_namespace_name() && !req->namespace_name().empty(), InvalidArgument, - "No namespace specified"); - SCHECK_GT(req->table_name_size(), 0, InvalidArgument, "No tables specified"); - if (pg_database_type) { - SCHECK_EQ( - req->pg_schema_name_size(), req->table_name_size(), InvalidArgument, - "Number of tables and number of pg schemas must match"); - } else { - SCHECK_EQ( - req->pg_schema_name_size(), 0, InvalidArgument, - "Pg Schema does not apply to CQL databases"); - } - - cdc::BootstrapProducerRequestPB bootstrap_req; - master::TSDescriptor* ts = nullptr; - for (int i = 0; i < req->table_name_size(); i++) { - string pg_schema_name = pg_database_type ? req->pg_schema_name(i) : ""; - auto table_info = GetTableInfoFromNamespaceNameAndTableName( - req->db_type(), req->namespace_name(), req->table_name(i), pg_schema_name); - SCHECK( - table_info, NotFound, Format("Table $0.$1$2 not found"), req->namespace_name(), - (pg_schema_name.empty() ? "" : pg_schema_name + "."), req->table_name(i)); - - bootstrap_req.add_table_ids(table_info->id()); - resp->add_table_ids(table_info->id()); - - // Pick a valid tserver to bootstrap from. - if (!ts) { - ts = VERIFY_RESULT(VERIFY_RESULT(table_info->GetTablets()).front()->GetLeader()); - } - } - SCHECK(ts, IllegalState, "No valid tserver found to bootstrap from"); - - std::shared_ptr proxy; - RETURN_NOT_OK(ts->GetProxy(&proxy)); - - cdc::BootstrapProducerResponsePB bootstrap_resp; - rpc::RpcController bootstrap_rpc; - bootstrap_rpc.set_deadline(rpc->GetClientDeadline()); - - RETURN_NOT_OK(proxy->BootstrapProducer(bootstrap_req, &bootstrap_resp, &bootstrap_rpc)); - if (bootstrap_resp.has_error()) { - RETURN_NOT_OK(StatusFromPB(bootstrap_resp.error().status())); - } - - resp->mutable_bootstrap_ids()->Swap(bootstrap_resp.mutable_cdc_bootstrap_ids()); - if (bootstrap_resp.has_bootstrap_time()) { - resp->set_bootstrap_time(bootstrap_resp.bootstrap_time()); - } - - return Status::OK(); -} - -Status CatalogManager::SetUniverseReplicationInfoEnabled( - const xcluster::ReplicationGroupId& replication_group_id, bool is_enabled) { - scoped_refptr universe; - { - SharedLock lock(mutex_); - - universe = FindPtrOrNull(universe_replication_map_, replication_group_id); - if (universe == nullptr) { - return STATUS( - NotFound, "Could not find CDC producer universe", replication_group_id, - MasterError(MasterErrorPB::OBJECT_NOT_FOUND)); - } + universe = FindPtrOrNull(universe_replication_map_, replication_group_id); + if (universe == nullptr) { + return STATUS( + NotFound, "Could not find CDC producer universe", replication_group_id, + MasterError(MasterErrorPB::OBJECT_NOT_FOUND)); + } } // Update the Master's Universe Config with the new state. @@ -5121,314 +3261,6 @@ Status CatalogManager::SetUniverseReplicationEnabled( return Status::OK(); } -Status CatalogManager::AlterUniverseReplication( - const AlterUniverseReplicationRequestPB* req, AlterUniverseReplicationResponsePB* resp, - rpc::RpcContext* rpc, const LeaderEpoch& epoch) { - LOG(INFO) << "Servicing AlterUniverseReplication request from " << RequestorString(rpc) << ": " - << req->ShortDebugString(); - - SCHECK_PB_FIELDS_NOT_EMPTY(*req, replication_group_id); - - RETURN_NOT_OK(ValidateUniverseUUID(req, *this)); - - auto replication_group_id = xcluster::ReplicationGroupId(req->replication_group_id()); - auto original_ri = GetUniverseReplication(replication_group_id); - SCHECK_EC_FORMAT( - original_ri, NotFound, MasterError(MasterErrorPB::OBJECT_NOT_FOUND), - "Could not find xCluster replication group $0", replication_group_id); - - // Currently, config options are mutually exclusive to simplify transactionality. - int config_count = (req->producer_master_addresses_size() > 0 ? 1 : 0) + - (req->producer_table_ids_to_remove_size() > 0 ? 1 : 0) + - (req->producer_table_ids_to_add_size() > 0 ? 1 : 0) + - (req->has_new_replication_group_id() ? 1 : 0) + - (!req->producer_namespace_id_to_remove().empty() ? 1 : 0); - SCHECK_EC_FORMAT( - config_count == 1, InvalidArgument, MasterError(MasterErrorPB::INVALID_REQUEST), - "Only 1 Alter operation per request currently supported: $0", req->ShortDebugString()); - - if (req->producer_master_addresses_size() > 0) { - return UpdateProducerAddress(original_ri, req); - } - - if (req->has_producer_namespace_id_to_remove()) { - return RemoveNamespaceFromReplicationGroup( - original_ri, req->producer_namespace_id_to_remove(), *this, epoch); - } - - if (req->producer_table_ids_to_remove_size() > 0) { - std::vector table_ids( - req->producer_table_ids_to_remove().begin(), req->producer_table_ids_to_remove().end()); - return master::RemoveTablesFromReplicationGroup(original_ri, table_ids, *this, epoch); - } - - if (req->producer_table_ids_to_add_size() > 0) { - RETURN_NOT_OK(AddTablesToReplication(original_ri, req, resp, rpc)); - xcluster_manager_->CreateXClusterSafeTimeTableAndStartService(); - return Status::OK(); - } - - if (req->has_new_replication_group_id()) { - return RenameUniverseReplication(original_ri, req); - } - - return Status::OK(); -} - -Status CatalogManager::UpdateProducerAddress( - scoped_refptr universe, const AlterUniverseReplicationRequestPB* req) { - CHECK_GT(req->producer_master_addresses_size(), 0); - - // TODO: Verify the input. Setup an RPC Task, ListTables, ensure same. - - { - // 1a. Persistent Config: Update the Universe Config for Master. - auto l = universe->LockForWrite(); - l.mutable_data()->pb.mutable_producer_master_addresses()->CopyFrom( - req->producer_master_addresses()); - - // 1b. Persistent Config: Update the Consumer Registry (updates TServers) - auto cluster_config = ClusterConfig(); - auto cl = cluster_config->LockForWrite(); - auto replication_group_map = - cl.mutable_data()->pb.mutable_consumer_registry()->mutable_producer_map(); - auto it = replication_group_map->find(req->replication_group_id()); - if (it == replication_group_map->end()) { - LOG(WARNING) << "Valid Producer Universe not in Consumer Registry: " - << req->replication_group_id(); - return STATUS( - NotFound, "Could not find CDC producer universe", req->ShortDebugString(), - MasterError(MasterErrorPB::OBJECT_NOT_FOUND)); - } - it->second.mutable_master_addrs()->CopyFrom(req->producer_master_addresses()); - cl.mutable_data()->pb.set_version(cl.mutable_data()->pb.version() + 1); - - { - // Need both these updates to be atomic. - auto w = sys_catalog_->NewWriter(leader_ready_term()); - RETURN_NOT_OK(w->Mutate( - QLWriteRequestPB::QL_STMT_UPDATE, universe.get(), cluster_config.get())); - RETURN_NOT_OK(CheckStatus( - sys_catalog_->SyncWrite(w.get()), - "Updating universe replication info and cluster config in sys-catalog")); - } - l.Commit(); - cl.Commit(); - } - - // 2. Memory Update: Change xcluster_rpc_tasks (Master cache) - { - auto result = universe->GetOrCreateXClusterRpcTasks(req->producer_master_addresses()); - if (!result.ok()) { - return result.status(); - } - } - - return Status::OK(); -} - -Status CatalogManager::AddTablesToReplication( - scoped_refptr universe, const AlterUniverseReplicationRequestPB* req, - AlterUniverseReplicationResponsePB* resp, rpc::RpcContext* rpc) { - SCHECK_GT(req->producer_table_ids_to_add_size(), 0, InvalidArgument, "No tables specified"); - - if (universe->IsDbScoped()) { - // We either add the entire namespace at once, or one table at a time as they get created. - if (req->has_producer_namespace_to_add()) { - SCHECK( - !req->producer_namespace_to_add().id().empty(), InvalidArgument, "Invalid Namespace Id"); - SCHECK( - !req->producer_namespace_to_add().name().empty(), InvalidArgument, - "Invalid Namespace name"); - SCHECK_EQ( - req->producer_namespace_to_add().database_type(), YQLDatabase::YQL_DATABASE_PGSQL, - InvalidArgument, "Invalid Namespace database_type"); - } else { - SCHECK_EQ( - req->producer_table_ids_to_add_size(), 1, InvalidArgument, - "When adding more than table to a DB scoped replication the namespace info must also be " - "provided"); - } - } else { - SCHECK( - !req->has_producer_namespace_to_add(), InvalidArgument, - "Cannot add namespaces to non DB scoped replication"); - } - - xcluster::ReplicationGroupId alter_replication_group_id(xcluster::GetAlterReplicationGroupId( - xcluster::ReplicationGroupId(req->replication_group_id()))); - - // If user passed in bootstrap ids, check that there is a bootstrap id for every table. - SCHECK( - req->producer_bootstrap_ids_to_add_size() == 0 || - req->producer_table_ids_to_add_size() == req->producer_bootstrap_ids_to_add().size(), - InvalidArgument, "Number of bootstrap ids must be equal to number of tables", - req->ShortDebugString()); - - // Verify no 'alter' command running. - scoped_refptr alter_ri; - { - SharedLock lock(mutex_); - alter_ri = FindPtrOrNull(universe_replication_map_, alter_replication_group_id); - } - { - if (alter_ri != nullptr) { - LOG(INFO) << "Found " << alter_replication_group_id << "... Removing"; - if (alter_ri->LockForRead()->is_deleted_or_failed()) { - // Delete previous Alter if it's completed but failed. - master::DeleteUniverseReplicationRequestPB delete_req; - delete_req.set_replication_group_id(alter_ri->id()); - master::DeleteUniverseReplicationResponsePB delete_resp; - Status s = DeleteUniverseReplication(&delete_req, &delete_resp, rpc); - if (!s.ok()) { - if (delete_resp.has_error()) { - resp->mutable_error()->Swap(delete_resp.mutable_error()); - return s; - } - return SetupError(resp->mutable_error(), s); - } - } else { - return STATUS( - InvalidArgument, "Alter for CDC producer currently running", req->ShortDebugString(), - MasterError(MasterErrorPB::INVALID_REQUEST)); - } - } - } - - // Map each table id to its corresponding bootstrap id. - std::unordered_map table_id_to_bootstrap_id; - if (req->producer_bootstrap_ids_to_add().size() > 0) { - for (int i = 0; i < req->producer_table_ids_to_add().size(); i++) { - table_id_to_bootstrap_id[req->producer_table_ids_to_add(i)] = - req->producer_bootstrap_ids_to_add(i); - } - - // Ensure that table ids are unique. We need to do this here even though - // the same check is performed by SetupUniverseReplication because - // duplicate table ids can cause a bootstrap id entry in table_id_to_bootstrap_id - // to be overwritten. - if (table_id_to_bootstrap_id.size() != - implicit_cast(req->producer_table_ids_to_add().size())) { - return STATUS( - InvalidArgument, - "When providing bootstrap ids, " - "the list of tables must be unique", - req->ShortDebugString(), MasterError(MasterErrorPB::INVALID_REQUEST)); - } - } - - // Only add new tables. Ignore tables that are currently being replicated. - auto tid_iter = req->producer_table_ids_to_add(); - std::unordered_set new_tables(tid_iter.begin(), tid_iter.end()); - auto original_universe_l = universe->LockForRead(); - auto& original_universe_pb = original_universe_l->pb; - - for (const auto& table_id : original_universe_pb.tables()) { - new_tables.erase(table_id); - } - if (new_tables.empty()) { - return STATUS( - InvalidArgument, "CDC producer already contains all requested tables", - req->ShortDebugString(), MasterError(MasterErrorPB::INVALID_REQUEST)); - } - - // 1. create an ALTER table request that mirrors the original 'setup_replication'. - master::SetupUniverseReplicationRequestPB setup_req; - master::SetupUniverseReplicationResponsePB setup_resp; - setup_req.set_replication_group_id(alter_replication_group_id.ToString()); - setup_req.mutable_producer_master_addresses()->CopyFrom( - original_universe_pb.producer_master_addresses()); - setup_req.set_transactional(original_universe_pb.transactional()); - - if (req->has_producer_namespace_to_add()) { - *setup_req.add_producer_namespaces() = req->producer_namespace_to_add(); - } - - for (const auto& table_id : new_tables) { - setup_req.add_producer_table_ids(table_id); - - // Add bootstrap id to request if it exists. - auto bootstrap_id = FindOrNull(table_id_to_bootstrap_id, table_id); - if (bootstrap_id) { - setup_req.add_producer_bootstrap_ids(*bootstrap_id); - } - } - - // 2. run the 'setup_replication' pipeline on the ALTER Table - Status s = SetupUniverseReplication(&setup_req, &setup_resp, rpc); - if (!s.ok()) { - if (setup_resp.has_error()) { - resp->mutable_error()->Swap(setup_resp.mutable_error()); - return s; - } - return SetupError(resp->mutable_error(), s); - } - - return Status::OK(); -} - -Status CatalogManager::RenameUniverseReplication( - scoped_refptr universe, const AlterUniverseReplicationRequestPB* req) { - CHECK(req->has_new_replication_group_id()); - - const xcluster::ReplicationGroupId old_replication_group_id(universe->id()); - const xcluster::ReplicationGroupId new_replication_group_id(req->new_replication_group_id()); - if (old_replication_group_id == new_replication_group_id) { - return STATUS( - InvalidArgument, "Old and new replication ids must be different", req->ShortDebugString(), - MasterError(MasterErrorPB::INVALID_REQUEST)); - } - - { - LockGuard lock(mutex_); - auto l = universe->LockForWrite(); - scoped_refptr new_ri; - - // Assert that new_replication_name isn't already in use. - if (FindPtrOrNull(universe_replication_map_, new_replication_group_id) != nullptr) { - return STATUS( - InvalidArgument, "New replication id is already in use", req->ShortDebugString(), - MasterError(MasterErrorPB::INVALID_REQUEST)); - } - - // Since the replication_group_id is used as the key, we need to create a new - // UniverseReplicationInfo. - new_ri = new UniverseReplicationInfo(new_replication_group_id); - new_ri->mutable_metadata()->StartMutation(); - SysUniverseReplicationEntryPB* metadata = &new_ri->mutable_metadata()->mutable_dirty()->pb; - metadata->CopyFrom(l->pb); - metadata->set_replication_group_id(new_replication_group_id.ToString()); - - // Also need to update internal maps. - auto cluster_config = ClusterConfig(); - auto cl = cluster_config->LockForWrite(); - auto replication_group_map = - cl.mutable_data()->pb.mutable_consumer_registry()->mutable_producer_map(); - (*replication_group_map)[new_replication_group_id.ToString()] = - std::move((*replication_group_map)[old_replication_group_id.ToString()]); - replication_group_map->erase(old_replication_group_id.ToString()); - - { - // Need both these updates to be atomic. - auto w = sys_catalog_->NewWriter(leader_ready_term()); - RETURN_NOT_OK(w->Mutate(QLWriteRequestPB::QL_STMT_DELETE, universe.get())); - RETURN_NOT_OK( - w->Mutate(QLWriteRequestPB::QL_STMT_UPDATE, new_ri.get(), cluster_config.get())); - RETURN_NOT_OK(CheckStatus( - sys_catalog_->SyncWrite(w.get()), - "Updating universe replication info and cluster config in sys-catalog")); - } - new_ri->mutable_metadata()->CommitMutation(); - cl.Commit(); - - // Update universe_replication_map after persistent data is saved. - universe_replication_map_[new_replication_group_id] = new_ri; - universe_replication_map_.erase(old_replication_group_id); - } - - return Status::OK(); -} - Status CatalogManager::GetUniverseReplication( const GetUniverseReplicationRequestPB* req, GetUniverseReplicationResponsePB* resp, rpc::RpcContext* rpc) { @@ -5457,87 +3289,6 @@ Status CatalogManager::GetUniverseReplication( return Status::OK(); } -/* - * Checks if the universe replication setup has completed. - * Returns Status::OK() if this call succeeds, and uses resp->done() to determine if the setup has - * completed (either failed or succeeded). If the setup has failed, then resp->replication_error() - * is also set. If it succeeds, replication_error() gets set to OK. - */ -Status CatalogManager::IsSetupUniverseReplicationDone( - const IsSetupUniverseReplicationDoneRequestPB* req, - IsSetupUniverseReplicationDoneResponsePB* resp, - rpc::RpcContext* rpc) { - LOG(INFO) << "IsSetupUniverseReplicationDone from " << RequestorString(rpc) << ": " - << req->DebugString(); - - SCHECK_PB_FIELDS_NOT_EMPTY(*req, replication_group_id); - - auto is_operation_done = VERIFY_RESULT(master::IsSetupUniverseReplicationDone( - xcluster::ReplicationGroupId(req->replication_group_id()), *this)); - - resp->set_done(is_operation_done.done()); - StatusToPB(is_operation_done.status(), resp->mutable_replication_error()); - return Status::OK(); -} - -Status CatalogManager::IsSetupNamespaceReplicationWithBootstrapDone( - const IsSetupNamespaceReplicationWithBootstrapDoneRequestPB* req, - IsSetupNamespaceReplicationWithBootstrapDoneResponsePB* resp, rpc::RpcContext* rpc) { - LOG(INFO) << Format( - "IsSetupNamespaceReplicationWithBootstrapDone $0: $1", RequestorString(rpc), - req->DebugString()); - - SCHECK(req->has_replication_group_id(), InvalidArgument, "Replication group ID must be provided"); - const xcluster::ReplicationGroupId replication_group_id(req->replication_group_id()); - - scoped_refptr bootstrap_info; - { - SharedLock lock(mutex_); - - bootstrap_info = FindPtrOrNull(universe_replication_bootstrap_map_, replication_group_id); - SCHECK( - bootstrap_info != nullptr, NotFound, - Format( - "Could not find universe replication bootstrap $0", replication_group_id.ToString())); - } - - // Terminal states are DONE or some failure state. - { - auto l = bootstrap_info->LockForRead(); - resp->set_state(l->state()); - - if (l->is_done()) { - resp->set_done(true); - StatusToPB(Status::OK(), resp->mutable_bootstrap_error()); - return Status::OK(); - } - - if (l->is_deleted_or_failed()) { - resp->set_done(true); - - if (!bootstrap_info->GetReplicationBootstrapErrorStatus().ok()) { - StatusToPB( - bootstrap_info->GetReplicationBootstrapErrorStatus(), resp->mutable_bootstrap_error()); - } else { - LOG(WARNING) << "Did not find setup universe replication bootstrap error status."; - StatusToPB(STATUS(InternalError, "unknown error"), resp->mutable_bootstrap_error()); - } - - // Add failed bootstrap to GC now that we've responded to the user. - { - LockGuard lock(mutex_); - replication_bootstraps_to_clear_.push_back(bootstrap_info->ReplicationGroupId()); - } - - return Status::OK(); - } - } - - // Not done yet. - resp->set_done(false); - return Status::OK(); -} - Status CatalogManager::UpdateConsumerOnProducerSplit( const UpdateConsumerOnProducerSplitRequestPB* req, UpdateConsumerOnProducerSplitResponsePB* resp, @@ -5670,16 +3421,22 @@ Status CatalogManager::UpdateConsumerOnProducerMetadata( schema_cached->Clear(); cdc::SchemaVersionsPB* schema_versions_pb = nullptr; + bool schema_versions_updated = false; // TODO (#16557): Support remove_table_id() for colocated tables / tablegroups. if (IsColocationParentTableId(consumer_table_id) && req->colocation_id() != kColocationIdNotSet) { auto map = stream_entry->mutable_colocated_schema_versions(); - schema_versions_pb = &((*map)[req->colocation_id()]); + schema_versions_pb = FindOrNull(*map, req->colocation_id()); + if (nullptr == schema_versions_pb) { + // If the colocation_id itself does not exist, it needs to be recorded in clusterconfig. + // This is to handle the case where source-target schema version mapping is 0:0. + schema_versions_updated = true; + schema_versions_pb = &((*map)[req->colocation_id()]); + } } else { schema_versions_pb = stream_entry->mutable_schema_versions(); } - bool schema_versions_updated = false; SchemaVersion current_producer_schema_version = schema_versions_pb->current_producer_schema_version(); SchemaVersion current_consumer_schema_version = @@ -6326,7 +4083,7 @@ void CatalogManager::RunXReplBgTasks(const LeaderEpoch& epoch) { WARN_NOT_OK(CleanUpDeletedXReplStreams(epoch), "Failed Cleaning Deleted XRepl Streams"); // Clean up Failed Universes on the Consumer. - WARN_NOT_OK(ClearFailedUniverse(), "Failed Clearing Failed Universe"); + WARN_NOT_OK(ClearFailedUniverse(epoch), "Failed Clearing Failed Universe"); // Clean up Failed Replication Bootstrap on the Consumer. WARN_NOT_OK(ClearFailedReplicationBootstrap(), "Failed Clearing Failed Replication Bootstrap"); @@ -6338,8 +4095,7 @@ void CatalogManager::RunXReplBgTasks(const LeaderEpoch& epoch) { StartXReplParentTabletDeletionTaskIfStopped(); } - -Status CatalogManager::ClearFailedUniverse() { +Status CatalogManager::ClearFailedUniverse(const LeaderEpoch& epoch) { // Delete a single failed universe from universes_to_clear_. if (PREDICT_FALSE(FLAGS_disable_universe_gc)) { return Status::OK(); @@ -6368,7 +4124,8 @@ Status CatalogManager::ClearFailedUniverse() { req.set_replication_group_id(replication_group_id.ToString()); req.set_ignore_errors(true); - RETURN_NOT_OK(DeleteUniverseReplication(&req, &resp, /* RpcContext */ nullptr)); + RETURN_NOT_OK( + xcluster_manager_->DeleteUniverseReplication(&req, &resp, /* RpcContext */ nullptr, epoch)); return Status::OK(); } @@ -6761,126 +4518,6 @@ Status CatalogManager::BumpVersionAndStoreClusterConfig( return Status::OK(); } -Status CatalogManager::ValidateTableSchemaForXCluster( - const client::YBTableInfo& info, const SetupReplicationInfo& setup_info, - GetTableSchemaResponsePB* resp) { - bool is_ysql_table = info.table_type == client::YBTableType::PGSQL_TABLE_TYPE; - if (setup_info.transactional && !GetAtomicFlag(&FLAGS_TEST_allow_ycql_transactional_xcluster) && - !is_ysql_table) { - return STATUS_FORMAT( - NotSupported, "Transactional replication is not supported for non-YSQL tables: $0", - info.table_name.ToString()); - } - - // Get corresponding table schema on local universe. - GetTableSchemaRequestPB req; - - auto* table = req.mutable_table(); - table->set_table_name(info.table_name.table_name()); - table->mutable_namespace_()->set_name(info.table_name.namespace_name()); - table->mutable_namespace_()->set_database_type( - GetDatabaseTypeForTable(client::ClientToPBTableType(info.table_type))); - - // Since YSQL tables are not present in table map, we first need to list tables to get the table - // ID and then get table schema. - // Remove this once table maps are fixed for YSQL. - ListTablesRequestPB list_req; - ListTablesResponsePB list_resp; - - list_req.set_name_filter(info.table_name.table_name()); - Status status = ListTables(&list_req, &list_resp); - SCHECK( - status.ok() && !list_resp.has_error(), NotFound, - Format("Error while listing table: $0", status.ToString())); - - const auto& source_schema = client::internal::GetSchema(info.schema); - for (const auto& t : list_resp.tables()) { - // Check that table name and namespace both match. - if (t.name() != info.table_name.table_name() || - t.namespace_().name() != info.table_name.namespace_name()) { - continue; - } - - // Check that schema name matches for YSQL tables, if the field is empty, fill in that - // information during GetTableSchema call later. - bool has_valid_pgschema_name = !t.pgschema_name().empty(); - if (is_ysql_table && has_valid_pgschema_name && - t.pgschema_name() != source_schema.SchemaName()) { - continue; - } - - // Get the table schema. - table->set_table_id(t.id()); - status = GetTableSchema(&req, resp); - SCHECK( - status.ok() && !resp->has_error(), NotFound, - Format("Error while getting table schema: $0", status.ToString())); - - // Double-check schema name here if the previous check was skipped. - if (is_ysql_table && !has_valid_pgschema_name) { - std::string target_schema_name = resp->schema().pgschema_name(); - if (target_schema_name != source_schema.SchemaName()) { - table->clear_table_id(); - continue; - } - } - - // Verify that the table on the target side supports replication. - if (is_ysql_table && t.has_relation_type() && t.relation_type() == MATVIEW_TABLE_RELATION) { - return STATUS_FORMAT( - NotSupported, "Replication is not supported for materialized view: $0", - info.table_name.ToString()); - } - - Schema consumer_schema; - auto result = SchemaFromPB(resp->schema(), &consumer_schema); - - // We now have a table match. Validate the schema. - SCHECK( - result.ok() && consumer_schema.EquivalentForDataCopy(source_schema), IllegalState, - Format( - "Source and target schemas don't match: " - "Source: $0, Target: $1, Source schema: $2, Target schema: $3", - info.table_id, resp->identifier().table_id(), info.schema.ToString(), - resp->schema().DebugString())); - break; - } - - SCHECK( - table->has_table_id(), NotFound, - Format( - "Could not find matching table for $0$1", info.table_name.ToString(), - (is_ysql_table ? " pgschema_name: " + source_schema.SchemaName() : ""))); - - // Still need to make map of table id to resp table id (to add to validated map) - // For colocated tables, only add the parent table since we only added the parent table to the - // original pb (we use the number of tables in the pb to determine when validation is done). - if (info.colocated) { - // We require that colocated tables have the same colocation ID. - // - // Backward compatibility: tables created prior to #7378 use YSQL table OID as a colocation ID. - auto source_clc_id = info.schema.has_colocation_id() - ? info.schema.colocation_id() - : CHECK_RESULT(GetPgsqlTableOid(info.table_id)); - auto target_clc_id = (resp->schema().has_colocated_table_id() && - resp->schema().colocated_table_id().has_colocation_id()) - ? resp->schema().colocated_table_id().colocation_id() - : CHECK_RESULT(GetPgsqlTableOid(resp->identifier().table_id())); - SCHECK( - source_clc_id == target_clc_id, IllegalState, - Format( - "Source and target colocation IDs don't match for colocated table: " - "Source: $0, Target: $1, Source colocation ID: $2, Target colocation ID: $3", - info.table_id, resp->identifier().table_id(), source_clc_id, target_clc_id)); - } - - SCHECK( - !xcluster_manager_->IsTableReplicationConsumer(table->table_id()), IllegalState, - "N:1 replication topology not supported"); - - return Status::OK(); -} - std::unordered_set CatalogManager::GetAllXReplStreamIds() const { SharedLock l(mutex_); std::unordered_set result; @@ -7106,5 +4743,75 @@ Status CatalogManager::RemoveTableFromCDCStreamMetadataAndMaps( return Status::OK(); } +void CatalogManager::InsertNewUniverseReplication(UniverseReplicationInfo& replication_group) { + LockGuard lock(mutex_); + universe_replication_map_[replication_group.ReplicationGroupId()] = + scoped_refptr(&replication_group); +} + +scoped_refptr CatalogManager::GetUniverseReplicationBootstrap( + const xcluster::ReplicationGroupId& replication_group_id) { + TRACE("Acquired catalog manager lock"); + SharedLock lock(mutex_); + + return FindPtrOrNull(universe_replication_bootstrap_map_, replication_group_id); +} + +void CatalogManager::InsertNewUniverseReplicationInfoBootstrapInfo( + UniverseReplicationBootstrapInfo& bootstrap_info) { + LockGuard lock(mutex_); + universe_replication_bootstrap_map_[bootstrap_info.ReplicationGroupId()] = + scoped_refptr(&bootstrap_info); +} + +void CatalogManager::MarkReplicationBootstrapForCleanup( + const xcluster::ReplicationGroupId& replication_group_id) { + LockGuard lock(mutex_); + replication_bootstraps_to_clear_.push_back(replication_group_id); +} + +Status CatalogManager::ReplaceUniverseReplication( + const UniverseReplicationInfo& old_replication_group, + UniverseReplicationInfo& new_replication_group, const ClusterConfigInfo& cluster_config, + const LeaderEpoch& epoch) { + DCHECK(old_replication_group.metadata().HasWriteLock()); + DCHECK(new_replication_group.metadata().HasWriteLock()); + DCHECK(cluster_config.metadata().HasWriteLock()); + + LockGuard lock(mutex_); + + SCHECK_FORMAT( + !universe_replication_map_.contains(new_replication_group.ReplicationGroupId()), + InvalidArgument, "New replication id $0 is already in use", + new_replication_group.ReplicationGroupId()); + + { + // Need all three updates to be atomic. + auto w = sys_catalog_->NewWriter(epoch.leader_term); + RETURN_NOT_OK(w->Mutate(QLWriteRequestPB::QL_STMT_DELETE, &old_replication_group)); + RETURN_NOT_OK( + w->Mutate(QLWriteRequestPB::QL_STMT_UPDATE, &new_replication_group, &cluster_config)); + RETURN_NOT_OK(CheckStatus( + sys_catalog_->SyncWrite(w.get()), + "Updating universe replication info and cluster config in sys-catalog")); + } + + // Update universe_replication_map after persistent data is saved. + universe_replication_map_[new_replication_group.ReplicationGroupId()] = + scoped_refptr(&new_replication_group); + universe_replication_map_.erase(old_replication_group.ReplicationGroupId()); + + return Status::OK(); +} + +void CatalogManager::RemoveUniverseReplicationFromMap( + const xcluster::ReplicationGroupId& replication_group_id) { + LockGuard lock(mutex_); + if (universe_replication_map_.erase(replication_group_id) < 1) { + LOG(DFATAL) << "Replication group " << replication_group_id + << " was already deleted from in-mem map"; + } +} + } // namespace master } // namespace yb diff --git a/src/yb/rocksdb/util/rate_limiter.cc b/src/yb/rocksdb/util/rate_limiter.cc index 026232faab17..168a1e417948 100644 --- a/src/yb/rocksdb/util/rate_limiter.cc +++ b/src/yb/rocksdb/util/rate_limiter.cc @@ -62,6 +62,9 @@ GenericRateLimiter::GenericRateLimiter(int64_t rate_bytes_per_sec, fairness_(fairness > 100 ? 100 : fairness), rnd_((uint32_t)time(nullptr)), leader_(nullptr) { + CHECK_GT(refill_bytes_per_period_, 0) + << "rate_bytes_per_sec * refill_period_us should be > 1000000"; + for (size_t q = 0; q < yb::kElementsInIOPriority; ++q) { total_requests_[q] = 0; total_bytes_through_[q] = 0; @@ -108,6 +111,15 @@ void GenericRateLimiter::SetBytesPerSecond(int64_t bytes_per_second) { } void GenericRateLimiter::Request(int64_t bytes, const yb::IOPriority priority) { + while (bytes > 0) { + int64_t bytes_to_request = std::min(GetSingleBurstBytes(), bytes); + assert(bytes_to_request > 0); + RequestInternal(bytes_to_request, priority); + bytes -= bytes_to_request; + } +} + +void GenericRateLimiter::RequestInternal(int64_t bytes, const yb::IOPriority priority) { assert(bytes <= refill_bytes_per_period_.load(std::memory_order_relaxed)); const auto pri = yb::to_underlying(priority); diff --git a/src/yb/rocksdb/util/rate_limiter.h b/src/yb/rocksdb/util/rate_limiter.h index 572a807f22bd..71606a94ed68 100644 --- a/src/yb/rocksdb/util/rate_limiter.h +++ b/src/yb/rocksdb/util/rate_limiter.h @@ -49,9 +49,10 @@ class GenericRateLimiter : public RateLimiter { void SetBytesPerSecond(int64_t bytes_per_second) override; // Request for token to write bytes. If this request can not be satisfied, - // the call is blocked. Caller is responsible to make sure - // bytes <= GetSingleBurstBytes() + // the call is blocked. If the request is bigger than GetSingleBurstBytes() then the call is + // broken up into multiple requests of the same priority. void Request(const int64_t bytes, const yb::IOPriority pri) override; + void RequestInternal(const int64_t bytes, const yb::IOPriority pri); int64_t GetSingleBurstBytes() const override { return refill_bytes_per_period_.load(std::memory_order_relaxed); diff --git a/src/yb/rocksdb/util/rate_limiter_test.cc b/src/yb/rocksdb/util/rate_limiter_test.cc index 01d27df35a29..95e29d927907 100644 --- a/src/yb/rocksdb/util/rate_limiter_test.cc +++ b/src/yb/rocksdb/util/rate_limiter_test.cc @@ -40,7 +40,32 @@ namespace rocksdb { class RateLimiterTest : public RocksDBTest {}; TEST_F(RateLimiterTest, StartStop) { - std::unique_ptr limiter(new GenericRateLimiter(100, 100, 10)); + ASSERT_DEATH( + std::unique_ptr limiter(new GenericRateLimiter(100, 100, 10)), + "Check failed: refill_bytes_per_period_ > 0"); + + std::unique_ptr limiter(new GenericRateLimiter(1000, 1000, 10)); +} + +TEST_F(RateLimiterTest, LargeRequests) { + // Allow 1000 bytes per second. This gives us 1 byte every micro second, and a request of 1000 + // should take 1s. + std::unique_ptr limiter(new GenericRateLimiter(1000, 1000, 10)); + + auto now = yb::CoarseMonoClock::Now(); + limiter->Request(1000, yb::IOPriority::kHigh); + auto duration_waited = yb::ToMilliseconds(yb::CoarseMonoClock::Now() - now); + ASSERT_GT(duration_waited, 500); + +#if defined(OS_MACOSX) + // MacOS tests are much slower, so use a larger timeout. + ASSERT_LT(duration_waited, 10000); +#else + ASSERT_LT(duration_waited, 1500); +#endif + + ASSERT_EQ(limiter->GetTotalBytesThrough(), 1000); + ASSERT_EQ(limiter->GetTotalRequests(), 1000); } #ifndef OS_MACOSX diff --git a/src/yb/tablet/tablet_metadata.cc b/src/yb/tablet/tablet_metadata.cc index 2bc4fa755a41..75c9577d83e6 100644 --- a/src/yb/tablet/tablet_metadata.cc +++ b/src/yb/tablet/tablet_metadata.cc @@ -703,6 +703,13 @@ Result RaftGroupMetadata::Load( return ret; } +Result RaftGroupMetadata::LoadFromPath( + FsManager* fs_manager, const std::string& path) { + RaftGroupMetadataPtr ret(new RaftGroupMetadata(fs_manager, "")); + RETURN_NOT_OK(ret->LoadFromDisk(path)); + return ret; +} + Result RaftGroupMetadata::TEST_LoadOrCreate( const RaftGroupMetadataData& data) { if (data.fs_manager->LookupTablet(data.raft_group_id)) { @@ -772,6 +779,18 @@ Result RaftGroupMetadata::GetTableInfoUnlocked(const TableId& tabl return iter->second; } +std::vector RaftGroupMetadata::GetColocatedTableInfos() const { + std::vector table_infos; + { + std::lock_guard lock(data_mutex_); + for (const auto& [_, table_info] : kv_store_.colocation_to_table) { + DCHECK(table_info->schema().has_colocation_id()); + table_infos.push_back(table_info); + } + } + return table_infos; +} + Result RaftGroupMetadata::GetTableInfoUnlocked(ColocationId colocation_id) const { if (colocation_id == kColocationIdNotSet) { return GetTableInfoUnlocked(primary_table_id_); @@ -965,7 +984,7 @@ Status RaftGroupMetadata::LoadFromSuperBlock(const RaftGroupReplicaSuperBlockPB& std::lock_guard lock(data_mutex_); // Verify that the Raft group id matches with the one in the protobuf. - if (superblock.raft_group_id() != raft_group_id_) { + if (!raft_group_id_.empty() && superblock.raft_group_id() != raft_group_id_) { return STATUS(Corruption, "Expected id=" + raft_group_id_ + " found " + superblock.raft_group_id(), superblock.DebugString()); diff --git a/src/yb/tablet/tablet_metadata.h b/src/yb/tablet/tablet_metadata.h index 2fcf8e0aaedd..0d7f4b34fd7c 100644 --- a/src/yb/tablet/tablet_metadata.h +++ b/src/yb/tablet/tablet_metadata.h @@ -313,6 +313,8 @@ class RaftGroupMetadata : public RefCountedThreadSafe, Result GetTableInfo(ColocationId colocation_id) const; Result GetTableInfoUnlocked(ColocationId colocation_id) const REQUIRES(data_mutex_); + std::vector GetColocatedTableInfos() const; + const RaftGroupId& raft_group_id() const { DCHECK_NE(state_, kNotLoadedYet); return raft_group_id_; diff --git a/src/yb/tablet/tablet_snapshots.cc b/src/yb/tablet/tablet_snapshots.cc index 0c5e8d47a5c9..1a93436d24cf 100644 --- a/src/yb/tablet/tablet_snapshots.cc +++ b/src/yb/tablet/tablet_snapshots.cc @@ -223,6 +223,10 @@ Env& TabletSnapshots::env() { return *metadata().fs_manager()->env(); } +FsManager* TabletSnapshots::fs_manager() { + return metadata().fs_manager(); +} + Status TabletSnapshots::CleanupSnapshotDir(const std::string& dir) { auto& env = this->env(); if (!env.FileExists(dir)) { @@ -361,21 +365,55 @@ Result TabletSnapshots::GenerateRestoreWriteBatch( } } +// Get the map of snapshot cotable ids to the current cotable ids. +// The restored flushed frontiers can have cotable ids that are different from current cotable ids. +// This map is used to update the cotable ids in the restored flushed frontiers. +Result TabletSnapshots::GetCotableIdsMap(const std::string& snapshot_dir) { + docdb::CotableIdsMap cotable_ids_map; + if (snapshot_dir.empty() || !metadata().colocated()) { + return cotable_ids_map; + } + auto snapshot_metadata_file = TabletMetadataFile(snapshot_dir); + if (!env().FileExists(snapshot_metadata_file)) { + return cotable_ids_map; + } + auto snapshot_metadata = + VERIFY_RESULT(RaftGroupMetadata::LoadFromPath(fs_manager(), snapshot_metadata_file)); + for (const auto& snapshot_table_info : snapshot_metadata->GetColocatedTableInfos()) { + auto current_table_info = metadata().GetTableInfo( + snapshot_table_info->schema().colocation_id()); + if (!current_table_info.ok()) { + if (!current_table_info.status().IsNotFound()) { + return current_table_info.status(); + } + LOG_WITH_PREFIX(WARNING) << "Table " << snapshot_table_info->table_id + << " not found: " << current_table_info.status(); + } else if ((*current_table_info)->cotable_id != snapshot_table_info->cotable_id) { + cotable_ids_map[snapshot_table_info->cotable_id] = (*current_table_info)->cotable_id; + } + } + if (!cotable_ids_map.empty()) { + LOG_WITH_PREFIX(INFO) << "Cotable ids map: " << yb::ToString(cotable_ids_map); + } + return cotable_ids_map; +} + Status TabletSnapshots::RestoreCheckpoint( - const std::string& dir, HybridTime restore_at, const RestoreMetadata& restore_metadata, + const std::string& snapshot_dir, HybridTime restore_at, const RestoreMetadata& restore_metadata, const docdb::ConsensusFrontier& frontier, bool is_pitr_restore, const OpId& op_id) { LongOperationTracker long_operation_tracker("Restore checkpoint", 5s); // The following two lines can't just be changed to RETURN_NOT_OK(PauseReadWriteOperations()): // op_pause has to stay in scope until the end of the function. - auto op_pauses = StartShutdownRocksDBs(DisableFlushOnShutdown(!dir.empty()), AbortOps::kTrue); + auto op_pauses = StartShutdownRocksDBs( + DisableFlushOnShutdown(!snapshot_dir.empty()), AbortOps::kTrue); std::lock_guard lock(create_checkpoint_lock()); const string db_dir = regular_db().GetName(); const std::string intents_db_dir = has_intents_db() ? intents_db().GetName() : std::string(); - if (dir.empty()) { + if (snapshot_dir.empty()) { // Just change rocksdb hybrid time limit, because it should be in retention interval. // TODO(pitr) apply transactions and reset intents. CompleteShutdownRocksDBs(op_pauses); @@ -385,7 +423,7 @@ Status TabletSnapshots::RestoreCheckpoint( RETURN_NOT_OK(DeleteRocksDBs(CompleteShutdownRocksDBs(op_pauses))); auto s = CopyDirectory( - &rocksdb_env(), dir, db_dir, UseHardLinks::kTrue, CreateIfMissing::kTrue); + &rocksdb_env(), snapshot_dir, db_dir, UseHardLinks::kTrue, CreateIfMissing::kTrue); if (PREDICT_FALSE(!s.ok())) { LOG_WITH_PREFIX(WARNING) << "Copy checkpoint files status: " << s; return STATUS(IllegalState, "Unable to copy checkpoint files", s.ToString()); @@ -402,7 +440,8 @@ Status TabletSnapshots::RestoreCheckpoint( docdb::RocksDBPatcher patcher(db_dir, rocksdb_options); RETURN_NOT_OK(patcher.Load()); - RETURN_NOT_OK(patcher.ModifyFlushedFrontier(frontier)); + RETURN_NOT_OK(patcher.ModifyFlushedFrontier( + frontier, VERIFY_RESULT(GetCotableIdsMap(snapshot_dir)))); if (restore_at) { RETURN_NOT_OK(patcher.SetHybridTimeFilter(std::nullopt, restore_at)); } @@ -434,14 +473,14 @@ Status TabletSnapshots::RestoreCheckpoint( need_flush = true; } - if (!dir.empty()) { - auto tablet_metadata_file = TabletMetadataFile(dir); + if (!snapshot_dir.empty()) { + auto snapshot_metadata_file = TabletMetadataFile(snapshot_dir); // Old snapshots could lack tablet metadata, so just do nothing in this case. - if (env().FileExists(tablet_metadata_file)) { - LOG_WITH_PREFIX(INFO) << "Merging metadata with restored: " << tablet_metadata_file + if (env().FileExists(snapshot_metadata_file)) { + LOG_WITH_PREFIX(INFO) << "Merging metadata with restored: " << snapshot_metadata_file << " , force overwrite of schema packing " << !is_pitr_restore; RETURN_NOT_OK(tablet().metadata()->MergeWithRestored( - tablet_metadata_file, + snapshot_metadata_file, is_pitr_restore ? dockv::OverwriteSchemaPacking::kFalse : dockv::OverwriteSchemaPacking::kTrue)); need_flush = true; @@ -461,7 +500,7 @@ Status TabletSnapshots::RestoreCheckpoint( return s; } - LOG_WITH_PREFIX(INFO) << "Checkpoint restored from " << dir; + LOG_WITH_PREFIX(INFO) << "Checkpoint restored from " << snapshot_dir; LOG_WITH_PREFIX(INFO) << "Re-enabling compactions"; s = tablet().EnableCompactions(&op_pauses.blocking_rocksdb_shutdown_start); if (!s.ok()) { diff --git a/src/yb/tablet/tablet_snapshots.h b/src/yb/tablet/tablet_snapshots.h index f700da6147fb..2db0286b3145 100644 --- a/src/yb/tablet/tablet_snapshots.h +++ b/src/yb/tablet/tablet_snapshots.h @@ -101,7 +101,7 @@ class TabletSnapshots : public TabletComponent { // Restore the RocksDB checkpoint from the provided directory. // Only used when table_type_ == YQL_TABLE_TYPE. Status RestoreCheckpoint( - const std::string& dir, HybridTime restore_at, const RestoreMetadata& metadata, + const std::string& snapshot_dir, HybridTime restore_at, const RestoreMetadata& metadata, const docdb::ConsensusFrontier& frontier, bool is_pitr_restore, const OpId& op_id); // Applies specified snapshot operation. @@ -109,12 +109,15 @@ class TabletSnapshots : public TabletComponent { Status CleanupSnapshotDir(const std::string& dir); Env& env(); + FsManager* fs_manager(); Status RestorePartialRows(SnapshotOperation* operation); Result GenerateRestoreWriteBatch( const tserver::TabletSnapshotOpRequestPB& request, docdb::DocWriteBatch* write_batch); + Result GetCotableIdsMap(const std::string& snapshot_dir); + std::string TEST_last_rocksdb_checkpoint_dir_; }; diff --git a/src/yb/tools/data-patcher.cc b/src/yb/tools/data-patcher.cc index 9733aa29089b..f49ff1f58fff 100644 --- a/src/yb/tools/data-patcher.cc +++ b/src/yb/tools/data-patcher.cc @@ -950,7 +950,7 @@ class ApplyPatch { frontier.set_history_cutoff_information( { HybridTime::FromMicros(kYugaByteMicrosecondEpoch), HybridTime::FromMicros(kYugaByteMicrosecondEpoch) }); - RETURN_NOT_OK(patcher.ModifyFlushedFrontier(frontier)); + RETURN_NOT_OK(patcher.ModifyFlushedFrontier(frontier, docdb::CotableIdsMap())); } else { LOG(INFO) << "We did not see RocksDB CURRENT or MANIFEST-... files in " << dir << ", skipping applying " << patched_path; diff --git a/src/yb/tools/yb-backup/yb-backup-cross-feature-test.cc b/src/yb/tools/yb-backup/yb-backup-cross-feature-test.cc index 5992fd462716..0e858bba3158 100644 --- a/src/yb/tools/yb-backup/yb-backup-cross-feature-test.cc +++ b/src/yb/tools/yb-backup/yb-backup-cross-feature-test.cc @@ -1850,10 +1850,10 @@ TEST_P( tserver::FlushTabletsRequestPB::COMPACT)); ASSERT_OK(RunBackupCommand( - {"--backup_location", backup_dir, "--keyspace", Format("ysql.$0", backup_db_name), + {"--backup_location", backup_dir, "--keyspace", Format("ysql.$0", restore_db_name), "restore"})); - SetDbName(backup_db_name); + SetDbName(restore_db_name); ASSERT_NO_FATALS( InsertRows(Format("INSERT INTO $0 VALUES (9,9,9), (10,10,10), (11,11,11)", table_name), 3));