From 190577d3c505f7a36bbd231c456e9cc2544287dc Mon Sep 17 00:00:00 2001 From: Ibrar Ahmed Date: Fri, 23 Jan 2026 17:39:10 +0500 Subject: [PATCH 1/2] SPOC-381: Remove SpockCtrl utility code and documentation Remove all SpockCtrl-related code, documentation, build infrastructure, and references from the codebase. This includes deleting the utils/spockctrl and spockctrl directories, removing all documentation files in docs/modify/spockctrl/, removing build targets from Makefile, removing navigation entries from mkdocs.yml, and removing references from README.md, docs/modify/index.md, and docs/spock_release_notes.md. --- Makefile | 20 +- README.md | 1 - docs/modify/index.md | 4 - docs/modify/spockctrl/index.md | 134 -- docs/modify/spockctrl/spockctrl_functions.md | 301 ---- docs/modify/spockctrl/spockctrl_json.md | 81 -- docs/modify/spockctrl/spockctrl_workflow.md | 451 ------ docs/spock_release_notes.md | 6 - mkdocs.yml | 5 - utils/spockctrl/Makefile | 68 - utils/spockctrl/README.md | 781 ----------- utils/spockctrl/build.sh | 52 - utils/spockctrl/include/conf.h | 54 - utils/spockctrl/include/dbconn.h | 27 - utils/spockctrl/include/logger.h | 38 - utils/spockctrl/include/node.h | 13 - utils/spockctrl/include/repset.h | 17 - utils/spockctrl/include/slot.h | 16 - utils/spockctrl/include/spock.h | 19 - utils/spockctrl/include/spockctrl.h | 5 - utils/spockctrl/include/sql.h | 8 - utils/spockctrl/include/sub.h | 19 - utils/spockctrl/include/util.h | 47 - utils/spockctrl/include/workflow.h | 46 - utils/spockctrl/spockctrl.json | 45 - utils/spockctrl/src/conf.c | 353 ----- utils/spockctrl/src/dbconn.c | 129 -- utils/spockctrl/src/logger.c | 99 -- utils/spockctrl/src/node.c | 947 ------------- utils/spockctrl/src/repset.c | 1012 -------------- utils/spockctrl/src/slot.c | 398 ------ utils/spockctrl/src/spock.c | 279 ---- utils/spockctrl/src/spockctrl.c | 217 --- utils/spockctrl/src/sql.c | 181 --- utils/spockctrl/src/sub.c | 1317 ------------------ utils/spockctrl/src/util.c | 388 ------ utils/spockctrl/src/workflow.c | 753 ---------- utils/spockctrl/workflows/add_4th_node.json | 339 ----- utils/spockctrl/workflows/add_node.json | 231 --- utils/spockctrl/workflows/remove_node.json | 66 - 40 files changed, 2 insertions(+), 8965 deletions(-) delete mode 100644 docs/modify/spockctrl/index.md delete mode 100644 docs/modify/spockctrl/spockctrl_functions.md delete mode 100644 docs/modify/spockctrl/spockctrl_json.md delete mode 100644 docs/modify/spockctrl/spockctrl_workflow.md delete mode 100644 utils/spockctrl/Makefile delete mode 100644 utils/spockctrl/README.md delete mode 100755 utils/spockctrl/build.sh delete mode 100644 utils/spockctrl/include/conf.h delete mode 100644 utils/spockctrl/include/dbconn.h delete mode 100644 utils/spockctrl/include/logger.h delete mode 100644 utils/spockctrl/include/node.h delete mode 100644 utils/spockctrl/include/repset.h delete mode 100644 utils/spockctrl/include/slot.h delete mode 100644 utils/spockctrl/include/spock.h delete mode 100644 utils/spockctrl/include/spockctrl.h delete mode 100644 utils/spockctrl/include/sql.h delete mode 100644 utils/spockctrl/include/sub.h delete mode 100644 utils/spockctrl/include/util.h delete mode 100644 utils/spockctrl/include/workflow.h delete mode 100644 utils/spockctrl/spockctrl.json delete mode 100644 utils/spockctrl/src/conf.c delete mode 100644 utils/spockctrl/src/dbconn.c delete mode 100644 utils/spockctrl/src/logger.c delete mode 100644 utils/spockctrl/src/node.c delete mode 100644 utils/spockctrl/src/repset.c delete mode 100644 utils/spockctrl/src/slot.c delete mode 100644 utils/spockctrl/src/spock.c delete mode 100644 utils/spockctrl/src/spockctrl.c delete mode 100644 utils/spockctrl/src/sql.c delete mode 100644 utils/spockctrl/src/sub.c delete mode 100644 utils/spockctrl/src/util.c delete mode 100644 utils/spockctrl/src/workflow.c delete mode 100644 utils/spockctrl/workflows/add_4th_node.json delete mode 100644 utils/spockctrl/workflows/add_node.json delete mode 100644 utils/spockctrl/workflows/remove_node.json diff --git a/Makefile b/Makefile index a55761ce..2faa943f 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ spock.control: spock.control.in include/spock.h sed 's/__SPOCK_VERSION__/$(spock_version)/;s/__REQUIRES__/$(requires)/' $(realpath $(srcdir)/spock.control.in) > $(control_path) -all: spock.control spockctrl +all: spock.control # ----------------------------------------------------------------------------- # Regression tests @@ -112,22 +112,6 @@ endef check_prove: $(prove_check) -# ----------------------------------------------------------------------------- -# SpockCtrl -# ----------------------------------------------------------------------------- -spockctrl: - $(MAKE) -C $(srcdir)/utils/spockctrl - -clean: clean-spockctrl - -clean-spockctrl: - $(MAKE) -C $(srcdir)/utils/spockctrl clean - -install: install-spockctrl - -install-spockctrl: - $(MAKE) -C $(srcdir)/utils/spockctrl install - # ----------------------------------------------------------------------------- # Dist packaging # ----------------------------------------------------------------------------- @@ -163,7 +147,7 @@ git-dist: dist-common # ----------------------------------------------------------------------------- # PHONY targets # ----------------------------------------------------------------------------- -.PHONY: all check regresscheck spock.control spockctrl clean clean-spockctrl \ +.PHONY: all check regresscheck spock.control clean \ dist git-dist check_prove valgrind-check define _spk_create_recursive_target diff --git a/README.md b/README.md index 696f4711..11a9d436 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ - [Modifying a Cluster](docs/modify/index.md) - [Monitoring your Cluster](docs/monitoring/index.md) - [Spock Functions](docs/spock_functions/index.md) -- [Using spockctrl Management Functions](docs/modify/spockctrl/index.md) - [Release Notes](docs/spock_release_notes.md) - [Limitations](docs/limitations.md) - [FAQ](docs/FAQ.md) diff --git a/docs/modify/index.md b/docs/modify/index.md index ffd14bb1..0fc64ec1 100644 --- a/docs/modify/index.md +++ b/docs/modify/index.md @@ -10,7 +10,3 @@ There are several tools and scripts you can use to add a node to a cluster; the [pgBackRest](add_node_pgbackrest.md) is an open-source tool that you can use add a node to a replicating cluster with minimal interruption. -* **Use Spockctrl to manage aspects of your Spock replication setup.** - -The [Spockctrl utility](../modify/spockctrl/index.md) is appropriate for use in production clusters that can't be taken out of production. - diff --git a/docs/modify/spockctrl/index.md b/docs/modify/spockctrl/index.md deleted file mode 100644 index f13b09b9..00000000 --- a/docs/modify/spockctrl/index.md +++ /dev/null @@ -1,134 +0,0 @@ -# Using Spockctrl to Manage a Cluster - -Spockctrl is a command-line utility designed to simplify the management of a multi-master replication setup for Postgres. It provides a convenient interface for common tasks such as: - -* **Node Management**: Creating, dropping, and listing nodes within your Spock cluster. -* **Replication Set Management**: Defining and modifying the replication sets that control which data is replicated. -* **Subscription Management**: Creating, dropping, and managing subscriptions between nodes to initiate and control replication. -* **Executing SQL**: Running ad-hoc SQL commands against your database nodes. -* **Workflow Automation**: Running predefined sets of operations (workflows) to perform complex tasks like adding a new node to the cluster. - -This section will guide you through the process of building, configuring, and using `spockctrl` to manage your Spock replication environment. - -Note that spockctrl is not a core component of the pgEdge Distributed or Enterprise Postgres product. - -## Installing and Configuring Spockctrl - -Spockctrl is built from source when you build the Spock extension; all supporting `spockctrl` objects are created as well, and copied into place in their respective locations. - -To simplify use, you can add the binary directory to your path and ensure that `LD_LIBRARY_PATH` is in your `.bashrc` file: - - ```bash -echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.bashrc && \ -echo 'export LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH"' >> ~/.bashrc -``` - -Alternatively, you can invoke `spockctrl` directly from the `spockctrl` directory by moving into the installation directory and prefixing commands with `./spockctrl`. - -### Configuration - Updating the spockctrl.json File - -Spockctrl uses a JSON configuration file to store database connection details and other settings. By default, `spockctrl` looks for a file named `spockctrl.json` in the directory where you run the `spockctrl` command. You can also specify a different configuration file using the `-c` or `--config=` command-line option. - -The configuration file contains the information necessary for `spockctrl` to connect to your Postgres instances and manage the Spock extension. Refer to the `spockctrl/spockctrl.json` file in the [source distribution](https://github.com/pgEdge/spock/blob/main/utils/spockctrl/spockctrl.json) for the most up-to-date structure and available options. - -!!! warning - - You should ensure that the `spockctrl.json` file has the correct permissions to prevent unauthorized access to database credentials. - -### Example: Manually Creating a Two-Node Cluster - -Before using spockctrl to deploy and configure a two-node (provider and subscriber) replication scenario, you must: - -1. Install and verify two Postgres instances. -2. Install and create (using `CREATE EXTENSION spock;`) the Spock extension on both instances. -3. Configure the `postgresql.conf` file on both nodes for logical replication. - (for example, set `wal_level = logical`, `shared_preload_libraries = 'spock'`, etc.). -4. Update the `pg_hba.conf` file on each instance to allow replication connections between the nodes and from the host running `spockctrl`. -5. Build `spock` in your environment, and optionally add the `spockctrl` executable to your path. -6. Update the `spockctrl.json` file. - -### Using `spockctrl` to Create a Cluster - -Assuming your `spockctrl.json` is in the current directory or is specified with `-c`: - -1. **Create the Provider Node:** - The following command tells Spock about your provider database instance: - - ```bash - spockctrl node create provider_node --dsn "host=pgserver1 port=5432 dbname=salesdb user=spock_user password=securepass" - ``` - -2. **Create the Subscriber Node:** - The following command registers your subscriber database instance: - ```bash - spockctrl node create subscriber_node --dsn "host=pgserver2 port=5432 dbname=salesdb_replica user=spock_user password=securepass" - ``` - -3. **Create a Replication Set on the Provider:** - The following command creates a replication data set named default_repset: - ```bash - spockctrl repset create default_repset --node provider_node - ``` - *(The `--node` might be implicit if your config ties repsets to nodes, or it might be needed to specify where the repset is being defined.)* - -4. **Add Tables and Sequences to the Replication Set:** - The following command adds our tables and sequences to the `default_repset` replication set: - ```bash - spockctrl repset add-table default_repset public.orders --node provider_node - spockctrl repset add-table default_repset public.customers --node provider_node - spockctrl repset add-seq default_repset public.order_id_seq --node provider_node - ``` - Alternatively, if you want to add all tables from the `public` schema: - ```bash - spockctrl repset add-all-tables default_repset public --node provider_node - ``` - -5. **Create the Subscription on the Subscriber:** - The following command initiates the replication process by telling the subscriber_node to connect to provider_node and subscribe to the specified replication sets: - ```bash - spockctrl sub create sales_subscription \ - --provider-dsn "host=pgserver1 port=5432 dbname=salesdb user=spock_user password=securepass" \ - --target-node subscriber_node \ - --repsets "default_repset" \ - --forward-origins "none" \ - --synchronize-data \ - --enable - ``` -Note that your command arguments will vary. In our example: -* `--provider-dsn` specifies a connection string, but you might instead use the node name defined in `spockctrl.json`. -* `--target-node` specifies where the subscription is created. -* `--synchronize-data` instructs spock to perform the initial data copy. -* `--enable` makes the subscription active immediately. - -6. **Wait for Initial Synchronization (Optional but Recommended):** - The following command confirms that the initial data copy is complete; when the copy is confirmed, replication should be operational: - - ```bash - spockctrl sub wait-for-sync sales_subscription --node subscriber_node - ``` - -7. **Check Subscription Status:** - You can use the following command to verify that the subscription is active and replicating: - - ```bash - spockctrl sub show-status sales_subscription --node subscriber_node - ``` - Look for a status `replicating` or `synchronized`. - -**Further Actions:** - -You can now: - -* Make changes to `public.orders` or `public.customers` on `provider_node` that will replicate to `subscriber_node`. -* Use `spockctrl sub disable sales_subscription` and `spockctrl sub enable sales_subscription` to pause and resume replication. -* Add more tables by modifying the replication set on the provider and then, if necessary, resynchronize the relevant tables or the subscription to the subscriber node. - - -## Using a Workflow to Add a Node to a Cluster - -`spockctrl` supports the execution of predefined workflows to automate complex multi-step operations. A workflow is typically a JSON file that defines a sequence of `spockctrl` commands or other actions. - -Sample workflow files are available in the [spockctrl GitHub repository](https://github.com/pgEdge/spock/tree/main/utils/spockctrl/workflows). - -For detailed information about using a workflow, and to review a walkthrough of a Spockctrl workfile, visit [here](spockctrl_workflow.md). - diff --git a/docs/modify/spockctrl/spockctrl_functions.md b/docs/modify/spockctrl/spockctrl_functions.md deleted file mode 100644 index 364f2cfb..00000000 --- a/docs/modify/spockctrl/spockctrl_functions.md +++ /dev/null @@ -1,301 +0,0 @@ -# Spockctrl Functions - -Spockctrl provides functions to manage different aspects of your Spock replication setup. The functions are grouped by the type of object they manage: - -* [Spockctrl node management](#spockctrl-node-management-functions) functions -* [Spockctrl replication management](#spockctrl-node-management-functions) functions -* [Spockctrl subscription management](#spockctrl-subscription-management-functions) functions -* [Spockctrl SQL execution](#spockctrl-sql-execution-functions) functions - -!!! info - -The exact subcommands and their options may vary slightly based on the `spockctrl` version. Use `spockctrl --help` for the most accurate and detailed information. - - -## Spockctrl Node Management Functions - -Use the `node` command to manage the nodes that participate in a replication cluster. The functions are: - -| Command | Description | -|---------|-------------| -| [spockctrl node create](#spockctrl-node-create) | Create a new node. | -| [spockctrl node drop](#spockctrl-node-drop) | Drop a node from a cluster. | -| [spockctrl node list](#spockctrl-node-list) | List the nodes in a cluster. | -| [spockctrl node add-interface](#spockctrl-node-add-interface) | Add an alternative connection interface to a node. | -| [spockctrl node drop-interface](#spockctrl-node-drop-interface) | Drop a connection interface to a node. | -| `-h`, `--help` | Displays a general help message listing the command options. | -| `--version` | Displays the version of the `spockctrl` utility. | - -### spockctrl node create - -Use `spockctrl node create` to create a new Spock node within the Spock metadata. The syntax is: - -`spockctrl node create --dsn ` - - * ``: A unique name for the node. - * `--dsn `: Specifies the connection string for the PostgreSQL instance. - -**Optional Arguments** - -* `-c `, `--config=` - Specifies the path to the `spockctrl.json` configuration file. If not provided, `spockctrl` looks for `spockctrl.json` in the current directory. For example: `spockctrl node list -c /etc/spock/spockctrl.conf` -* `-f `, `--format=` - Determines the output format for commands that display data. Specify: `table` (the default) to outputs data in a human-readable tabular format or `json` to output data in JSON format, which is useful for scripting or integration with other tools. For example: `spockctrl sub list --format=json` -* `-v `, `--verbose=` - Enables verbose logging to provide more detailed output about what `spockctrl` is doing. The verbosity level is an integer with higher numbers providing more detailed logs. `0` logs ERRORs only, `1` logs WARNINGs and ERRORs, `2` logs informational messages, WARNINGs, and ERRORs, and `3` logs debug level messages (most verbose). For example: `spockctrl --verbose=2 node create mynode ...` -* `-w `, `--workflow=` - Executes a predefined workflow from the specified JSON file. When using this option, you typically don't specify other commands like `node` or `sub` directly on the command line, as the workflow file directs the operations. For example: `spockctrl --config=myconfig.json --workflow=workflows/add_node.json` -* `-h`, `--help` - Displays a general help message listing all available commands, or help for a specific command or subcommand. For example, to request help for the `node create` subcommand: `spockctrl node create --help` - -**Example** - -`spockctrl node create my_node --dsn "host=pg1 port=5432 dbname=testdb user=spock"` - -### spockctrl node drop - -Use `spockctrl node drop` to remove a Spock node. The syntax is: - -`spockctrl node drop ` - * ``: A unique name for the node. - -**Example** - -`spockctrl node drop my_node` - -### spockctrl node list - -Use `spockctrl node list` to list all configured Spock nodes. The syntax is: - -`spockctrl node list` - -**Example** - -`spockctrl node list` - -### spockctrl node add-interface - -Use `spockctrl node add-interface` to add an alternative connection interface to a node. The syntax is: - -`spockctrl node add-interface --dsn ` - * ``: The name of the node accessed by the interface. - * ``: A unique name for the new interface. - * `--dsn `: Specifies the connection string of the new interface. - -**Example** - -`spockctrl node add-interface my_node secondary_conn --dsn "host=pg1_alt_ip port=5432 dbname=testdb"` - -### spockctrl node drop-interface - -Use `spockctrl node drop-interface` to drop an interface from a node. The syntax is: - -`spockctrl node drop-interface ` drops an interface from a node. - * ``: The name of the node accessed by the interface. - * ``: A unique name for the new interface. - -**Example** - -`spockctrl node drop-interface my_node secondary_conn` - - -## Replication Set Management Functions - -You can use the `spockctrl repset` command to manage the Spock replication sets that participate in a replication cluster. A replication set defines groups of tables and sequences to be replicated. - -| Command | Description | -|---------|-------------| -| [spockctrl repset create](#spockctrl-repset-create) | Create a new replication set. | -| [spockctrl repset drop](#spockctrl-repset-drop) | Drop a replication set. | -| [spockctrl repset add-table](#spockctrl-repset-add-table) | Add a specific table to a replication set. | -| [spockctrl repset add-all-tables](#spockctrl-repset-add-all-tables) | Add all tables from the specified schema to a replication set. | -| [spockctrl repset add-seq](#spockctrl-repset-add-seq) | Add a sequence to a replication set. | -| [spockctrl repset add-all-sequences](#spockctrl-repset-add-all-sequences) | Add all sequences from a given schema. | -| [spockctrl repset list](#spockctrl-repset-list) | List the available replication sets. | -| `-h`, `--help` | Displays a general help message listing all available commands, or help for a specific command or subcommand. For example, to request help for the `node create` subcommand: `spockctrl node create --help` | -| `--version` | Displays the version of the `spockctrl` utility. For example: `spockctrl --version` | - -### spockctrl repset create - -Use `spockctrl repset create` to create a new replication set. The syntax is: - -`spockctrl repset create [options]` - * ``: Name for the replication set (e.g., `default`, `custom_set`). - * You can include `options` to control DDL replication, DML replication, etc. (for example, `--replicate_insert=true`, `--replicate_update=false`). - -**Example** - -`spockctrl repset create my_repset` - -### spockctrl repset drop - -Use `spockctrl repset drop` to drop a replication set; the syntax is: - -`spockctrl repset drop ` - * ``: Name for the replication set (e.g., `default`, `custom_set`). - -**Example** - -`spockctrl repset drop my_repset` - -### spockctrl repset add-table - -Use `spockctrl repset add-table` to add a specific table to a replication set. The syntax is: - -`spockctrl repset add-table [options]` - * ``: Name for the replication set (e.g., `default`, `custom_set`). - * `` is the schema-qualified table name (e.g., `public.my_table`). - * You can include `options` to specify which columns to replicate and filtering conditions. - -**Example** - -`spockctrl repset add-table my_repset public.orders --columns "order_id,product_id,quantity"` - -### spockctrl repset add-all-tables - -Use `spockctrl repset add-all-tables` to add all tables from the specified schema to a replication set. The syntax is: - -`spockctrl repset add-all-tables ` - * ``: Name for the replication set (e.g., `default`, `custom_set`). - * ``: The name of the schema (e.g., `public`). - -**Example** - -`spockctrl repset add-all-tables my_repset public` - -### spockctrl repset add-seq - -Use `spockctrl repset add-seq` to add a sequence to a replication set. The syntax is: - -`spockctrl repset add-seq ` - * ``: Name for the replication set (e.g., `default`, `custom_set`). - * ``: The name of the sequence. - -**Example** - -`spockctrl repset add-seq my_repset public.my_sequence` - -### spockctrl repset add-all-sequences - -Use `spockctrl repset add-all-sequences` to adds all sequences from a given schema. The syntax is: - -`spockctrl repset add-all-sequences ` - * ``: Name for the replication set (e.g., `default`, `custom_set`). - * ``: The name of the schema (e.g., `public`). - -**Example** - -`spockctrl repset add-all-sequences my_repset public` - -### spockctrl repset list - -Use spockctrl repset list to list the available replication sets. The syntax is: - -`spockctrl repset list` - - -## Spockctrl Subscription Management Functions - -The `sub` command manages subscriptions, which connect a subscriber node to a provider node and initiate replication. - -| Command | Description | -|---------|-------------| -| [spockctrl sub create](#spockctrl-sub-create) | Create a new subscription. | -| [spockctrl sub drop](#spockctrl-sub-drop) | Drop the specified subscription. | -| [spockctrl sub enable](#spockctrl-sub-enable) | Enable the specified subscription. | -| [spockctrl sub disable](#spockctrl-sub-disable) | Enable the specified subscription. | -| [spockctrl sub list](#spockctrl-sub-list) | Generate a list of subscriptions. | -| [spockctrl sub wait-for-sync](#spockctrl-sub-wait-for-sync) | Wait for a subscription sync event. | -| [spockctrl sub show-status](#spockctrl-sub-show-status) | Display the status of the specified subscription. | -| `-h`, `--help` | Displays a general help message listing all available commands, or help for a specific command or subcommand. For example, to request help for the `node create` subcommand: `spockctrl sub create --help` | -| `--version` | Displays the version of the `spockctrl` utility. For example: `spockctrl --version` | - - -### spockctrl sub create -Use `spockctrl sub create [options]` to create a new subscription. - * ``: A unique name for the subscription. - * ``: The DSN of the provider node to subscribe to. - * You can include `options` to specifying replication sets, synchronization options, etc. - -**Example** - -`spockctrl sub create sub_n3_n1 "host=pg1 port=5432 dbname=testdb user=spock" --repsets "default,my_repset"` - -### spockctrl sub drop - -Use `spockctrl sub drop` to drop a subscription. The syntax is: - -`spockctrl sub drop ` - * ``: A unique name for the subscription. - -**Example** - -`spockctrl sub drop sub_n3_n1` - -### spockctrl sub enable - -Use `spockctrl sub enable` to enable a disabled subscription. The syntax is: - -`spockctrl sub enable ` - * ``: A unique name for the subscription. - -**Example** - -`spockctrl sub enable sub_n3_n1` - -### spockctrl sub disable - -Use `spockctrl sub disable` to disable an active subscription, pausing replication. The syntax is: - -`spockctrl sub disable ` - * ``: A unique name for the subscription. - -**Example** - -`spockctrl sub disable sub_n3_n1` - -### spockctrl sub list - -Use `spockctrl sub list` to list all subscriptions. The syntax is: - -`spockctrl sub list` - -### spockctrl sub show-status - -Use `spockctrl sub show-status` to show the status of a specific subscription. The syntax is: - -`spockctrl sub show-status ` - * ``: A unique name for the subscription. - -**Example** - -`spockctrl sub show-status sub_n3_n1` - -### spockctrl sub wait-for-sync - -Use `spockctrl sub wait-for-sync` to wait for a subscription to complete its initial data synchronization. - -`spockctrl sub wait-for-sync ` - -**Example** - -`spockctrl sub wait-for-sync sub_n3_n1` - -## Spockctrl SQL Execution Functions - -The `sql` command allows you to execute arbitrary SQL commands on a specified node. This can be useful for performing administrative tasks or querying Spock-specific metadata. - -| Command | Description | -|---------|-------------| -| [spockctrl sql](#spockctrl-sql) | Use the spockctrl sql command to execute a SQL command. | -| `-h`, `--help` | Displays a general help message listing all available commands, or help for a specific command or subcommand. For example, to request help for the `spockctrl sql` subcommand: `spockctrl sql --help` | -| `--version` | Display the version of the `spockctrl` utility. For example: `spockctrl --version` | - -### spockctrl sql - -Use the `spockctrl sql` command to execute a SQL command. The syntax is: - -`spockctrl sql ` - * `` is the name of the node (from `spockctrl.json`) on which to execute the command. - * `` is the SQL query or command to run. - -**Example** - -`spockctrl sql my_node "SELECT * FROM spock.node;"` -`spockctrl sql my_node "CALL spock.sub_resync_table('my_subscription', 'public.my_table');"` - diff --git a/docs/modify/spockctrl/spockctrl_json.md b/docs/modify/spockctrl/spockctrl_json.md deleted file mode 100644 index 55b4d66a..00000000 --- a/docs/modify/spockctrl/spockctrl_json.md +++ /dev/null @@ -1,81 +0,0 @@ -# Populating the spockctrl.json File - -Before using Spockctrl to add a node to your cluster, your cluster information must be added to the `spockctrl.json` file. The file template is available in the [spock repository](https://github.com/pgEdge/spock/blob/main/utils/spockctrl/spockctrl.json). - -!!! warning - - You should ensure that the `spockctrl.json` file has the correct permissions to prevent unauthorized access to database credentials. - -Within the template, provide information about your cluster and each node that is in your cluster. If you are using Spockctrl to add a node to your cluster, you should also add the node details for the new node to this file before invoking `spockctrl`: - -## spockctrl.json Properties - -Use properties within the `spockctrl.json` file describe your cluster before invoking `spockctrl`: - -| Property | Description | -|----------|-------------| -| Global Properties | Use properties in the `global` section to describe your cluster. | -| spock -> cluster_name | The name of your cluster. | -| spock -> version | The pgEdge .json file version in use. | -| log -> log_level | Specify the message severity level to use for Spockctrl; valid options are: `0` (log errors only), `1` (log warnings and errors), `2` (log informational messages, warnings, and errors), and `3` (log debug level messages (most verbose)). | -| log -> log_destination | Specify the target destination for your log messages. | -| log -> log_file | Specify the log file name for your log files. | -| spock-nodes Properties | Provide a stanza about each node in your cluster in the `spock-nodes` section. If you are adding a node to your cluster, update this file to add the connection information for the new node before invoking `spockctrl`. | -| spock-nodes -> node_name | The unique name of a cluster node. | -| spock-nodes -> postgres -> postgres_ip | The IP address used for connections to the Postgres server on this node. | -| spock-nodes -> postgres -> postgres_port | The Postgres listener port used for connections to the Postgres server. | -| spock-nodes -> postgres -> postgres_user | The Postgres user that will be used for server connections. | -| spock-nodes -> postgres -> postgres_password | The password associated with the specified Postgres user. | -| spock-nodes -> postgres -> postgres_db | The name of the Postgres database. | - -### Example - spockctrl.json Content - -The following is sample content from a `spockctrl.json` file; customize the `spockctrl.json` file to contain connection information about your cluster. - -```json -{ - "global": { - "spock": { - "cluster_name": "pgedge", - "version": "1.0.0" - }, - "log": { - "log_level": "INFO", - "log_destination": "console", - "log_file": "/var/log/spockctrl.log" - } - }, - "spock-nodes": [ - { - "node_name": "n1", - "postgres": { - "postgres_ip": "127.0.0.1", - "postgres_port": 5431, - "postgres_user": "pgedge", - "postgres_password": "pgedge", - "postgres_db": "pgedge" - } - }, - { - "node_name": "n2", - "postgres": { - "postgres_ip": "127.0.0.1", - "postgres_port": 5432, - "postgres_user": "pgedge", - "postgres_password": "pgedge", - "postgres_db": "pgedge" - } - }, - { - "node_name": "n3", - "postgres": { - "postgres_ip": "127.0.0.1", - "postgres_port": 5433, - "postgres_user": "pgedge", - "postgres_password": "pgedge", - "postgres_db": "pgedge" - } - } - ] -} -``` diff --git a/docs/modify/spockctrl/spockctrl_workflow.md b/docs/modify/spockctrl/spockctrl_workflow.md deleted file mode 100644 index f293e349..00000000 --- a/docs/modify/spockctrl/spockctrl_workflow.md +++ /dev/null @@ -1,451 +0,0 @@ -# Writing a Workflow File - -To use `spockctrl` to make an automated change to your cluster, you need to describe the changes in a workflow file. A workflow file describes the changes you'll be making to your cluster in a step-by-step, node-by-node manner, and needs to be customized for your cluster. - -!!! note - - The new node should not be accessible to users while adding a node with a workflow. - - Do not modify your cluster's DDL during node addition. - - All nodes in your cluster must be available for the duration of the addition. - - If the workflow fails, don't invoke the workflow again until you ensure that all artifacts created by previous run of the workflow have been removed! - - -Sample workflows are available in the Spock extension's Github repository for common activities; the samples can help you: - -* `add_node.json` - Add a node to a cluster. -* `remove_node.json` - Remove a node from a cluster. - -To execute a workflow, include the `-w` (or `--workflow=`) command-line option when you invoke `spockctrl`, followed by the path to the workflow JSON file. - -```bash -spockctrl --config=/path/to/my/spockctrl.json --workflow=/path/to/my/workflow.json -``` -or -```bash -spockctrl -c path/to/my/spockctrl.json -w path/to/my/workflow.json -``` - -## Example - Workflow to Add a Node to a Two-Node Cluster - -In this example, we'll walk through the stanzas that make up a workflow that adds a new node to a two-node cluster. Within the workflow file, the `COMMAND` property identifies the action performed by the stanza in which it is used. Spock 5.0 supports the following `COMMANDs`: - -| COMMAND | Description | -|---------|-------------| -| CREATE NODE | Add a node to a cluster. | -| DROP NODE | Drop a node from a cluster. | -| CREATE SUBSCRIPTION | Add a subscription to a cluster. | -| DROP SUBSCRIPTION| Drop a subscription from a cluster. | -| CREATE REPSET | Add a repset to a cluster. | -| DROP REPSET | Drop a node to a cluster. | -| CREATE SLOT | Add a replication slot. | -| DROP SLOT | Drop a replication slot. | -| ENABLE SUBSCRIPTION | Start replication on a node. | -| DISABLE SUBSCRIPTION | Stop replication on a node. | -| SQL | Invoke the specified Postgres SQL command. | - -!!! info - - In this walkthrough, we're using a two-node cluster; if your cluster is larger than two nodes, any actions performed on the replica node (in our example, `n2`) should be performed on *every* replica node in your cluster. A replica node is any existing node that is not used as a source node. - -Our sample workflow adds a third node to a new node cluster. The first stanza provides connection information for the host of the new node. Spockctrl can add only one node per workflow file; provide this information for each new node you add to your cluster. - -This stanza associates the name of the new node with the connection properties of the new node: - -* Provide the new node name in the `node` property and the `--node_name` property - the name must be identical. -* `--dsn` specifies the connection properties of the new node. - -```json -{ - "workflow_name": "Add Node", - "description": "Adding third node (n3) to two node (n1,n2) cluster.", - "steps": [ - { - "spock": { - "node": "n3", - "command": "CREATE NODE", - "description": "Create a spock node n3", - "args": [ - "--node_name=n3", - "--dsn=host=127.0.0.1 port=5433 user=pgedge password=pgedge", - "--location=Los Angeles", - "--country=USA", - "--info={\"key\": \"value\"}" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` -The next stanza creates the subscription from the new node (`n3`) to the source node (`n1`). In this stanza: - -* the `node` property specifies the name of the node in our original cluster that is used as our source node. The content of this node will be copied to the new node that we are creating. -* `--sub_name` specifies the name of the new subscription. -* `--provider_dsn` specifies the connection properties of the provider (our new node). -* `--replication_sets` specifies the names of the replication sets created for the subscription. -* `--enabled`, `--synchronize_data` and `--synchronize_structure` must be `true`. - -```json - { - "spock": { - "node": "n1", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n3_n1) on (n1) for n3->n1", - "sleep": 0, - "args": [ - "--sub_name=sub_n3_n1", - "--provider_dsn=host=127.0.0.1 port=5433 user=pgedge password=spockpass", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=true", - "--synchronize_data=true", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -Our next stanza creates subscriptions between the new node and any existing replica nodes. - -* `node` specifies the name of the existing replica node. -* `--provider_dsn` specifies the connection properties of the new node; this is the provider node for the new subscription. -* `--replication_sets` specifies the names of the replication sets created for the subscription -* `--enabled`, `--synchronize_data` and `--synchronize_structure` must be `true`. - -!!! info - - You will need to include this stanza once for each replica node in your cluster; if your existing cluster has three nodes (one source node and two replica nodes), you will add two copies of this stanza. If your existing cluster has four nodes (one source node and three replica nodes), you will add three copies of this stanza. - -```json - { - "spock": { - "node": "n2", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n3_n2) on (n2) for n3->n2", - "sleep": 0, - "args": [ - "--sub_name=sub_n3_n2", - "--provider_dsn=host=127.0.0.1 port=5433 user=pgedge password=spockpass", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=true", - "--synchronize_data=true", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "ignore_errors": false, - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next step, we wait for the apply worker to check state of the subscription. This step is optional, but should be considered a *best practice*. This step uses a SQL command to call the `spock.wait_for_apply_worker` function. - -* `$n2.sub_create` is a variable (the subscription ID) populated by the previous `CREATE SUBSCRIPTION` stanza. -* If needed, use the `sleep` property to accomodate processing time. - -!!! info - - Note that if you have more than one replica node, and multiple `CREATE SUBSCRIPTION` stanzas, each stanza should be followed by a copy of this stanza, with the variable reset for each subsequent execution (`$n3.sub_create`, `$n4.sub_create`, etc). - -```json - { - "sql": { - "node": "n2", - "command": "SQL", - "description": "Wait for apply worker on n2 subscription", - "sleep": 0, - "args": [ - "--sql=SELECT spock.wait_for_apply_worker($n2.sub_create, 1000);" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In our next stanza, we create a subscription between each replica node (`n2`) and our new node (`n3`). - -* `--provider_dsn` is the connection string of the replica node (in our example, the provider is the first node referenced in our subscription). -* `--enabled`, `--synchronize_data`, `--force_text_transfer` and `--synchronize_structure` must be `false`. - -!!! info - - If you have multiple replica nodes, you will need one iteration of this stanza for each replica node in your cluster. - -```json - { - "spock": { - "node": "n3", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n2_n3) on (n3) for n2->n3", - "sleep": 5, - "args": [ - "--sub_name=sub_n2_n3", - "--provider_dsn=host=127.0.0.1 port=5432 user=pgedge password=spockpass", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=false" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we use a `CREATE SLOT` command to create a replication slot for the new subscription between our existing replica (`n2`) and our new node (`n3`). Provide the slot name in the form `spk_database-name_node-name_subscription-name` where: - -* `database-name` is the name of your database. -* `node-name` name of the existing replica node. -* `subscription-name` is the subscription created in the previous step. - -!!! info - - You must create one replication slot for each iteration of the CREATE SUBSCRIPTION stanza that allows a replica node to communicate with the new node; if you have three nodes in your cluster (one source node, and two replica nodes), you will need to provide one slot for each replica node (or two slots total). - -```json - { - "spock": { - "node": "n2", - "command": "CREATE SLOT", - "description": "Create a logical replication slot spk_pgedge_n2_sub_n2_n3 on (n2)", - "args": [ - "--slot=spk_pgedge_n2_sub_n2_n3", - "--plugin=spock_output" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -Next, on each replica node, we invoke a SQL command that starts a sync event between the existing node(`n2`) and our source node (`n1`). This ensures that our replica nodes stay in sync with the source node for the duration of the ADD NODE process. - -The function (`spock.sync_event`) returns the log sequence number (LSN) of the sync event: - -```json - { - "sql": { - "node": "n2", - "command": "SQL", - "description": "Trigger a sync event on (n2)", - "sleep": 10, - "args": [ - "--sql=SELECT spock.sync_event();" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -The previous stanza returns the LSN of the sync event in the `$n2.sync_event` variable; in the next stanza, we watch for that variable so we know when the step is complete. - -* `node` specifies the cluster source node (in our example, `n1`). -* `$n2.sync_event` is the LSN returned by the previous stanza. -* If needed, you can use `sleep` to provide extra processing time for the step. - -!!! info - - Include this stanza once for each iteration of the previous stanza within your workflow file. - - -```json - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Wait for a sync event on (n1) for n2-n1", - "sleep": 0, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n2', '$n2.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we create a subscription on our new target node (`n3`) from the source node (`n1`): - -* `arg->--sub_name` is `sub_n1_n3`. -* `arg->--provider_dsn` is the connection string for the source node (our provider, `n1`). -* `--enabled`, `--synchronize_data`, and `--synchronize_structure` must be `true`. -* `--force_text_transfer` must be `false`. - -```json - { - "spock": { - "node": "n3", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n1_n3) for n1 fpr n1->n3", - "sleep": 0, - "args": [ - "--sub_name=sub_n1_n3", - "--provider_dsn=host=127.0.0.1 port=5431 user=pgedge password=spockpass", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=true", - "--synchronize_data=true", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -Then, we include a stanza that triggers a sync event on the source node (`n1`) between the source node and the new node (`n3`). Use the `sleep` property to allocate time for the data to sync to the new node if needed. - -```json - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Trigger a sync event on (n1)", - "sleep": 5, - "args": [ - "--sql=SELECT spock.sync_event();" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we wait for the sync event started in the previous stanza to complete. Use the `sleep` property to allocate time for the data to sync to the new node if needed. - -```json - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Wait for a sync event on (n1) for n1-n3", - "sleep": 10, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n1', '$n1.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we check for data lag between the new node (n3) and any replica nodes in our cluster (n2). The timestamp returned is passed to the next stanza for use in evaluating the comparative state of the replica nodes and our new node. - -```json - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Check commit timestamp for n3 lag", - "sleep": 1, - "args": [ - "--sql=SELECT commit_timestamp FROM spock.lag_tracker WHERE origin_name = 'n2' AND receiver_name = 'n3'" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we use the timestamp from the previous stanza (`$n3.commit_timestamp`) to advance our replica slot to that location within our log files. This effectively advances transactions to the time specified (preventing duplicate entries from being written to the replica node). - -```json - { - "sql": { - "node": "n2", - "command": "SQL", - "description": "Advance the replication slot for n2->n3 based on a specific commit timestamp", - "sleep": 0, - "args": [ - "--sql=WITH lsn_cte AS (SELECT spock.get_lsn_from_commit_ts('spk_pgedge_n2_sub_n2_n3', '$n3.commit_timestamp'::timestamp) AS lsn) SELECT pg_replication_slot_advance('spk_pgedge_n2_sub_n2_n3', lsn) FROM lsn_cte;" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -Then, we enable the subscription from each replica node to the new node. At this point replication starts between any existing node and the new node (`n3`). - -```json - { - "spock": { - "node": "n3", - "command": "ENABLE SUBSCRIPTION", - "description": "Enable subscription (sub_n2_n3) on n3", - "args": [ - "--sub_name=sub_n2_n3", - "--immediate=true" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -After starting replication, we check the lag time between the new node and each node in the cluster. This step invokes a SQL command that loops through each node in the cluster and compares the lag on each node until each returned value is comparable. Use the `sleep` property to extend processing time if needed. - -```json - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Advance the replication slot for n2->n3 based on a specific commit timestamp", - "sleep": 0, - "args": [ - "--sql=DO $$\nDECLARE\n lag_n1_n3 interval;\n lag_n2_n3 interval;\nBEGIN\n LOOP\n SELECT now() - commit_timestamp INTO lag_n1_n3\n FROM spock.lag_tracker\n WHERE origin_name = 'n1' AND receiver_name = 'n3';\n\n SELECT now() - commit_timestamp INTO lag_n2_n3\n FROM spock.lag_tracker\n WHERE origin_name = 'n2' AND receiver_name = 'n3';\n\n RAISE NOTICE 'n1 → n3 lag: %, n2 → n3 lag: %',\n COALESCE(lag_n1_n3::text, 'NULL'),\n COALESCE(lag_n2_n3::text, 'NULL');\n\n EXIT WHEN lag_n1_n3 IS NOT NULL AND lag_n2_n3 IS NOT NULL\n AND extract(epoch FROM lag_n1_n3) < 59\n AND extract(epoch FROM lag_n2_n3) < 59;\n\n PERFORM pg_sleep(1);\n END LOOP;\nEND\n$$;\n" - ], - "on_success": {}, - "on_failure": {} - } - } - ] -} -``` - -The SQL command from the last step (in a more readable format) is: - -```sql - DO $$ - DECLARE - lag_n1_n3 interval; - lag_n2_n3 interval; - BEGIN - LOOP - SELECT now() - commit_timestamp INTO lag_n1_n3 - FROM spock.lag_tracker - WHERE origin_name = 'n1' AND receiver_name = 'n3'; - - SELECT now() - commit_timestamp INTO lag_n2_n3 - FROM spock.lag_tracker - WHERE origin_name = 'n2' AND receiver_name = 'n3'; - - RAISE NOTICE 'n1 -> n3 lag: %, n2 -> n3 lag: %', - COALESCE(lag_n1_n3::text, 'NULL'), - COALESCE(lag_n2_n3::text, 'NULL'); - - EXIT WHEN lag_n1_n3 IS NOT NULL AND lag_n2_n3 IS NOT NULL - AND extract(epoch FROM lag_n1_n3) < 59 - AND extract(epoch FROM lag_n2_n3) < 59; - - PERFORM pg_sleep(1); - END LOOP; - END - $$; -``` \ No newline at end of file diff --git a/docs/spock_release_notes.md b/docs/spock_release_notes.md index 28b9ae4e..6fb67af6 100644 --- a/docs/spock_release_notes.md +++ b/docs/spock_release_notes.md @@ -65,12 +65,6 @@ Now no restriction exists. Spock will use memory until memory is exhausted (impr * Exception handling performance improvements are now managed with the spock.exception_replay_queue_size GUC. * Previously, replication lag was estimated on the source node; this meant that if there were no transactions being replicated, the reported lag could continue to increase. Lag tracking is now calculated at the target node, with improved accuracy. * Spock 5.0 implements LSN Checkpointing with `spock.sync()` and `spock.wait_for_sync_event()`. This feature allows you to identify a checkpoint in the source node WAL files, and watch for the LSN of the checkpoint on a replica node. This allows you to guarantee that a DDL change, has replicated from the source node to all other nodes before publishing an update. -* The `spockctrl` command line utility and sample workflows simplify the management of a Spock multi-master replication setup for PostgreSQL. `spockctrl` provides a convenient interface for: - * node management - * replication set management - * subscription management - * ad-hoc SQL execution - * workflow automation * Previously, replicated `DELETE` statements that attempted to delete a *missing* row were logged as exceptions. Since the purpose of a `DELETE` statement is to remove a row, we no longer log these as exceptions. Instead these are now logged in the `Resolutions` table. * `INSERT` conflicts resulting from a duplicate primary key or identity replica are now transformed into an `UPDATE` that updates all columns of the existing row, using Last-Write-Wins (LWW) logic. The transaction is then logged in the node’s `Resolutions` table, as either: * `keep local` if the local node’s `INSERT` has a later timestamp than the arriving `INSERT` diff --git a/mkdocs.yml b/mkdocs.yml index 42fda7d3..d1c6fae8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -71,11 +71,6 @@ nav: - Using Zodan Scripts and Workflows: modify/zodan/zodan_readme.md - Adding a Node with Zero Downtime: modify/zodan/zodan_tutorial.md - Adding a Node with Minimal Downtime with pgBackRest: modify/add_node_pgbackrest.md - - Using Spockctrl: - - Modifying your Cluster with Spockctrl: modify/spockctrl/index.md - - Using Spockctrl Functions: modify/spockctrl/spockctrl_functions.md - - Populating the spockctrl.json File: modify/spockctrl/spockctrl_json.md - - Using a Workflow File to Modify a Cluster: modify/spockctrl/spockctrl_workflow.md - Monitoring a Cluster: - Monitoring the Configuration and Health of a Cluster: monitoring/index.md - Finding Cluster Information: monitoring/spock_info.md diff --git a/utils/spockctrl/Makefile b/utils/spockctrl/Makefile deleted file mode 100644 index 6a298702..00000000 --- a/utils/spockctrl/Makefile +++ /dev/null @@ -1,68 +0,0 @@ -# Compiler -CC = $(shell $(PG_CONFIG) --cc) - -# PostgreSQL include and library directories -PG_INCDIR = $(shell $(PG_CONFIG) --includedir) -PG_LIBDIR = $(shell $(PG_CONFIG) --libdir) -PG_BINDIR = $(shell $(PG_CONFIG) --bindir) - -PG_SHAREDIR = $(shell $(PG_CONFIG) --sharedir) -SPOCK_DIR = $(PG_SHAREDIR)/spock - -UNAME_S := $(shell uname -s) -ifeq ($(UNAME_S),Linux) - RPATH = -Wl,--enable-new-dtags,-rpath,$(PG_LIBDIR) -else ifeq ($(UNAME_S),Darwin) - RPATH = -Wl,-rpath,$(PG_LIBDIR) -else - RPATH = -endif - -# PostgreSQL library -PG_LIBS = -lpq - -# JSON library using pkg-config -JSON_CFLAGS = $(shell pkg-config --cflags jansson) -JSON_LIBS = $(shell pkg-config --libs jansson) - -# Compiler flags -CFLAGS += -Wall -g3 -O0 -pthread -I./include -I$(PG_INCDIR) $(JSON_CFLAGS) - -# Source files -SRCS = src/spockctrl.c src/util.c src/sql.c src/slot.c src/logger.c src/workflow.c src/node.c src/repset.c src/sub.c src/dbconn.c src/conf.c - -# Object files -OBJS = $(SRCS:.c=.o) - -# Executable name -EXEC = spockctrl - -# Default target -all: $(EXEC) - -# Link object files to create executable -$(EXEC): $(OBJS) - $(CC) $(CFLAGS) -L$(PG_LIBDIR) -o $@ $^ $(PG_LIBS) $(JSON_LIBS) $(RPATH) - -# Compile source files to object files -%.o: %.c - $(CC) $(CFLAGS) -c $< -o $@ - -# Clean up build files -clean: - rm -f $(OBJS) $(EXEC) - -# Clean up all build files and backups -clean-all: clean - find . \( -name '*~' -o -name '*.o' -o -name 'spockctrl' -o -name '*.log' -o -name '*.tmp' \) -exec rm -f {} + - -installdirs: - @: - -install: - install -d $(DESTDIR)$(PG_BINDIR) - install -m 0755 $(EXEC) $(DESTDIR)$(PG_BINDIR)/ - install -d $(DESTDIR)$(SPOCK_DIR) - install -d $(DESTDIR)$(SPOCK_DIR)/workflows/ - find . -type f -name '*.json' ! -name 'spockctrl.json' -exec install -m 644 {} $(DESTDIR)$(SPOCK_DIR)/workflows/ \; - find . -type f -name 'spockctrl.json' -exec install -m 644 {} $(DESTDIR)$(SPOCK_DIR)/ \; diff --git a/utils/spockctrl/README.md b/utils/spockctrl/README.md deleted file mode 100644 index aa103de5..00000000 --- a/utils/spockctrl/README.md +++ /dev/null @@ -1,781 +0,0 @@ -# Spockctrl Tutorial - -`spockctrl` is a command-line utility designed to simplify the management of a Spock multi-master replication setup for PostgreSQL. It provides a convenient interface for common tasks such as: - -* **Node Management**: Creating, dropping, and listing nodes within your Spock cluster. -* **Replication Set Management**: Defining and modifying the replication sets that control which data is replicated. -* **Subscription Management**: Creating, dropping, and managing subscriptions between nodes to initiate and control replication. -* **Executing SQL**: Running ad-hoc SQL commands against your database nodes. -* **Workflow Automation**: Running predefined sets of operations (workflows) to perform complex tasks like adding a new node to the cluster. - -This tutorial will guide you through the process of building, configuring, and using `spockctrl` to manage your Spock replication environment. - -## Building and Installing Spockctrl - -Spockctrl is built from source when you build the Spock extension; all supporting `spockctrl` objects are created as well, and copied into place in their respective locations. For ease of use, you can copy the `spockctrl` executable to a directory included in your system's `PATH` environment variable (e.g., `/usr/local/bin/`). - - ```bash -sudo cp spockctrl /usr/local/bin/ -``` - -Alternatively, you can invoke `spockctrl` directly from the `spockctrl` directory by prefixing commands with `./spockctrl`. - -### Configuration - The spockctrl.json File - -Spockctrl uses a JSON configuration file to store database connection details and other settings. By default, `spockctrl` looks for a file named `spockctrl.json` in the directory where you run the `spockctrl` command. You can also specify a different configuration file using the `-c` or `--config=` command-line option. - -The configuration file contains the information necessary for `spockctrl` to connect to your PostgreSQL instances and manage the Spock extension. Use properties within the `spockctrl.json` file describe your cluster before invoking `spockctrl`: - -| Property | Description | -|----------|-------------| -| Global Properties | Use properties in the `global` section to describe your cluster. | -| spock -> cluster_name | The name of your cluster. | -| spock -> version | The .json file version in use. | -| log -> log_level | Specify the [message severity level](https://www.postgresql.org/docs/17/runtime-config-logging.html#RUNTIME-CONFIG-SEVERITY-LEVELS) to use for Spockctrl. | -| log -> log_destination | Specify the target destination for your log messages. | -| log -> log_file | Specify the log file name for your log files. | -| spock-nodes Properties | Provide a stanza about each node in your cluster in the `spock-nodes` section. If you are adding a node to your cluster, update this file to add the connection information for the new node before invoking `spockctrl`. | -| spock-nodes -> node_name | The unique name of a cluster node. | -| spock-nodes -> postgres -> postgres_ip | The IP address used for connections to the Postgres server on this node. | -| spock-nodes -> postgres -> postgres_port | The Postgres listener port used for connections to the Postgres server. | -| spock-nodes -> postgres -> postgres_user | The Postgres user that will be used for server connections. | -| spock-nodes -> postgres -> postgres_password | The password associated with the specified Postgres user. | -| spock-nodes -> postgres -> postgres_db | The name of the Postgres database. | - -### Example - spockctrl.json Content - -The following is sample content from a `spockctrl.json` file; customize the `spockctrl.json` file to contain connection information about your cluster. - -```json -{ - "global": { - "spock": { - "cluster_name": "my_cluster", - "version": "1.0.0" - }, - "log": { - "log_level": "INFO", - "log_destination": "console", - "log_file": "/var/log/spockctrl.log" - } - }, - "spock-nodes": [ - { - "node_name": "n1", - "postgres": { - "postgres_ip": "127.0.0.1", - "postgres_port": 5431, - "postgres_user": "my_username", - "postgres_password": "my_password", - "postgres_db": "my_database" - } - }, - { - "node_name": "n2", - "postgres": { - "postgres_ip": "127.0.0.1", - "postgres_port": 5432, - "postgres_user": "my_username", - "postgres_password": "my_password", - "postgres_db": "my_database" - } - }, - { - "node_name": "n3", - "postgres": { - "postgres_ip": "127.0.0.1", - "postgres_port": 5433, - "postgres_user": "my_username", - "postgres_password": "my_password", - "postgres_db": "my_database" - } - } - ] -} -``` - -**Important:** -* Ensure the `spockctrl.json` file has the correct permissions to prevent unauthorized access to database credentials. -* Always refer to the `spockctrl/spockctrl.json` file in the source distribution for the most up-to-date structure and available options. You can use this file as a starting point for your own configurations. - -## Basic Commands - -`spockctrl` provides commands to manage different aspects of your Spock replication setup. Most commands follow a `spockctrl [options]` structure. You can always get help for a specific command by typing `spockctrl --help` or `spockctrl --help`. - -### Node Management (`node`) - -The `node` command is used to manage the Spock nodes (PostgreSQL instances) that participate in replication. - -* **`spockctrl node create --dsn `** creates a new Spock node representation within the Spock metadata. - * ``: A unique name for the node. - * `--dsn`: The connection string for the PostgreSQL instance. - * Example: `spockctrl node create provider1 --dsn "host=pg1 port=5432 dbname=testdb user=spock"` - -* **`spockctrl node drop `** removes a Spock node. - * Example: `spockctrl node drop provider1` - -* **`spockctrl node list`** lists all configured Spock nodes. - * Example: `spockctrl node list` - -* **`spockctrl node add-interface --dsn `** adds an alternative connection interface to a node. - * Example: `spockctrl node add-interface provider1 secondary_conn --dsn "host=pg1_alt_ip port=5432 dbname=testdb"` - -* **`spockctrl node drop-interface `** drops an interface from a node. - * Example: `spockctrl node drop-interface provider1 secondary_conn` - -### Replication Set Management (`repset`) - -The `repset` command manages replication sets; a replication set defines groups of tables and sequences to be replicated by your cluster. - -* **`spockctrl repset create [options]`** creates a new replication set. - * ``: Name for the replication set (e.g., `default`, `custom_set`). - * Options can control DDL replication, DML replication, etc. (e.g., `--replicate_insert=true`, `--replicate_update=false`). - * Example: `spockctrl repset create my_tables_repset` - -* **`spockctrl repset drop `** drops a replication set. - * Example: `spockctrl repset drop my_tables_repset` - -* **`spockctrl repset add-table [options]`** adds a specific table to a replication set. - * `` is the schema-qualified table name (e.g., `public.my_table`). - * Options allow you to specify which columns to replicate or row filtering conditions. - * Example: `spockctrl repset add-table my_tables_repset public.orders --columns "order_id,product_id,quantity"` - -* **`spockctrl repset add-all-tables `** adds all tables from a given schema to a replication set. - * ``: The name of the schema (e.g., `public`). - * Example: `spockctrl repset add-all-tables default_repset public` - -* **`spockctrl repset add-seq `** adds a sequence to a replication set. - * Example: `spockctrl repset add-seq default_repset public.my_sequence` - -* **`spockctrl repset add-all-sequences `** adds all sequences from a given schema. - * Example: `spockctrl repset add-all-sequences default_repset public` - -* **`spockctrl repset list`** lists available replication sets. - -### Subscription Management (`sub`) - -The `sub` command manages subscriptions, which connect a subscriber node to a provider node and initiate replication. - -* **`spockctrl sub create [options]`** creates a new subscription. - * ``: A unique name for the subscription. - * ``: The DSN of the provider node to subscribe to. - * Options include specifying replication sets, synchronization options, etc. - * Example: `spockctrl sub create sub_to_provider1 "host=pg1 port=5432 dbname=testdb user=spock" --repsets "default,my_tables_repset"` - -* **`spockctrl sub drop `** drops a subscription. - * Example: `spockctrl sub drop sub_to_provider1` - -* **`spockctrl sub enable `** enables a disabled subscription. - * Example: `spockctrl sub enable sub_to_provider1` - -* **`spockctrl sub disable `** disables an active subscription, pausing replication. - * Example: `spockctrl sub disable sub_to_provider1` - -* **`spockctrl sub list`** lists all subscriptions. - -* **`spockctrl sub show-status `** shows the status of a specific subscription. - * Example: `spockctrl sub show-status sub_to_provider1` - -* **`spockctrl sub wait-for-sync `** waits for a subscription to complete its initial data synchronization. - * Example: `spockctrl sub wait-for-sync sub_to_provider1` - -### SQL Execution (`sql`) - -The `sql` command allows you to execute arbitrary SQL commands on a specified node. This can be useful for administrative tasks or querying Spock-specific metadata. - -* **`spockctrl sql `** executes a SQL command. - * `` is the name of the node (from `spockctrl.json`) on which to execute the command. - * `` is the SQL query or command to run. - * Example: `spockctrl sql provider1 "SELECT * FROM spock.node;"` - * Example: `spockctrl sql subscriber1 "CALL spock.sub_resync_table('my_subscription', 'public.my_table');"` - -**Note:** The exact subcommands and their options may vary slightly based on the `spockctrl` version. Always use `spockctrl --help` for the most accurate and detailed information. - - -## Command-Line Options - -`spockctrl` supports several global command-line options that can be used with most commands: - -* **`-c `, `--config=`** specifies the path to the `spockctrl.json` configuration file. If not provided, `spockctrl` looks for `spockctrl.json` in the current directory. - * Example: `spockctrl node list -c /etc/spock/spockctrl.conf` - -* **`-f `, `--format=`** determines the output format for commands that display data. - * `table`: (Default) Outputs data in a human-readable tabular format. - * `json`: Outputs data in JSON format, which is useful for scripting or integration with other tools. - * Example: `spockctrl sub list --format=json` - -* **`-v `, `--verbose=`** enables verbose logging to provide more detailed output about what `spockctrl` is doing. The verbosity level can be an integer (e.g., `0`, `1`, `2`, `3`), with higher numbers typically meaning more detailed logs. - * `0`: Errors only (or default behavior if not specified). - * `1`: Warnings and errors. - * `2`: Informational messages, warnings, and errors. - * `3`: Debug level messages (most verbose). - * Example: `spockctrl --verbose=2 node create mynode ...` - -* **`-w `, `--workflow=`** executes a predefined workflow from the specified JSON file. When using this option, you typically don't specify other commands like `node` or `sub` directly on the command line, as the workflow file directs the operations. - * Example: `spockctrl --config=myconfig.json --workflow=workflows/add_node.json` - -* **`-h`, `--help`** displays a general help message listing all available commands, or help for a specific command or subcommand. - * Example (general help): `spockctrl --help` - * Example (help for `node` command): `spockctrl node --help` - * Example (help for `node create` subcommand): `spockctrl node create --help` - -* **`--version`** displays the version of the `spockctrl` utility. - * Example: `spockctrl --version` - -These options provide flexibility in how you interact with `spockctrl` and how it integrates into your operational procedures. - -## Example: Setting up a Two-Node Replication Scenario - -Before using spockctrl to deploy and configure a two-node (provider and subscriber) replication scenario, you must: - -1. Install and verify two PostgreSQL instances. -2. Install and create (using `CREATE EXTENSION spock;`) the Spock extension on both instances. -3. Configure the `postgresql.conf` file on both nodes for logical replication - (for example, set `wal_level = logical`, `shared_preload_libraries = 'spock'`, etc.). -4. Update the `pg_hba.conf` file on each instance to allow replication connections between the nodes and from the host running `spockctrl`. -5. Build `spock` in your environment, and optionally add the `spockctrl` executable to your path. -6. Update the `spockctrl.json` file. - -**Example `spockctrl.json`:** - -Let's assume your `spockctrl.json` looks something like this: - -```json -{ - "nodes": { - "provider_node": { - "dsn": "host=pgserver1 port=5432 dbname=salesdb user=spock_user password=securepass", - "is_provider": true - }, - "subscriber_node": { - "dsn": "host=pgserver2 port=5432 dbname=salesdb_replica user=spock_user password=securepass", - "is_subscriber": true - } - }, - "repsets": { - "default": { - "tables": ["public.orders", "public.customers"], - "sequences": ["public.order_id_seq"] - } - }, - "subscriptions": { - "sales_subscription": { - "provider_node": "provider_node", - "subscriber_node": "subscriber_node", - "repsets": ["default"], - "enabled": true - } - } -} -``` -*(Note: The actual structure of your `spockctrl.json` file might differ; this is a conceptual example based on common usage. Refer to `spockctrl/spockctrl.json` for configuration options.)* - -**Invoking `spockctrl` to Create a Cluster** - -Assuming your `spockctrl.json` is in the current directory or is specified with `-c`: - -1. **Create the Provider Node:** - The following command tells Spock about your provider database instance: - ```bash - spockctrl node create provider_node --dsn "host=pgserver1 port=5432 dbname=salesdb user=spock_user password=securepass" - ``` - -2. **Create the Subscriber Node:** - The following command registers your subscriber database instance: - ```bash - spockctrl node create subscriber_node --dsn "host=pgserver2 port=5432 dbname=salesdb_replica user=spock_user password=securepass" - ``` - -3. **Create a Replication Set on the Provider:** - The following command creates a replication data set named default_repset: - ```bash - spockctrl repset create default_repset --node provider_node - ``` - *(The `--node` might be implicit if your config ties repsets to nodes, or it might be needed to specify where the repset is being defined.)* - -4. **Add Tables and Sequences to the Replication Set:** - The following command adds our tables and sequences to the `default_repset` replication set: - ```bash - spockctrl repset add-table default_repset public.orders --node provider_node - spockctrl repset add-table default_repset public.customers --node provider_node - spockctrl repset add-seq default_repset public.order_id_seq --node provider_node - ``` - Alternatively, if you want to add all tables from the `public` schema: - ```bash - spockctrl repset add-all-tables default_repset public --node provider_node - ``` - -5. **Create the Subscription on the Subscriber:** - The following command initiates the replication process by telling the subscriber_node to connect to provider_node and subscribe to the specified replication sets: - ```bash - spockctrl sub create sales_subscription \ - --provider-dsn "host=pgserver1 port=5432 dbname=salesdb user=spock_user password=securepass" \ - --target-node subscriber_node \ - --repsets "default_repset" \ - --forward-origins "none" \ - --synchronize-data \ - --enable - ``` -Note that your command arguments will vary. In our example: -* `--provider-dsn` specifies a connection string, but you might instead use the node name defined in `spockctrl.json`. -* `--target-node` specifies where the subscription is created. -* `--synchronize-data` instructs spock to perform the initial data copy. -* `--enable` makes the subscription active immediately. - -6. **Wait for Initial Synchronization (Optional but Recommended):** - The following command confirms that the initial data copy is complete; when the copy is confirmed, replication should be operational: - ```bash - spockctrl sub wait-for-sync sales_subscription --node subscriber_node - ``` - -7. **Check Subscription Status:** - You can use the following command to verify that the subscription is active and replicating: - ```bash - spockctrl sub show-status sales_subscription --node subscriber_node - ``` - Look for a status like 'replicating' or 'synchronized'. - -**Further Actions:** - -You can now: - -* Make changes to `public.orders` or `public.customers` on `provider_node` that will replicate to `subscriber_node`. -* Use `spockctrl sub disable sales_subscription` and `spockctrl sub enable sales_subscription` to pause and resume replication. -* Add more tables by modifying the replication set on the provider and then, if necessary, resynchronize the relevant tables or the subscription to the subscriber node. - - -## Using a Workflow to Add a Node to a Cluster - -`spockctrl` supports the execution of predefined workflows to automate complex multi-step operations. A workflow is typically a JSON file that defines a sequence of `spockctrl` commands or other actions. - -**Running a Workflow:** - -You can execute a workflow using the `-w` (or `--workflow=`) command-line option, followed by the path to the workflow JSON file. - -```bash -spockctrl --config=/path/to/my/spockctrl.json --workflow=/path/to/my/workflow.json -``` -or -```bash -spockctrl -c path/to/my/spockctrl.json -w path/to/my/workflow.json -``` - -**Workflow Structure:** - -Workflow files are JSON documents that outline the steps to be performed. Each step might involve: -* Executing `spockctrl` commands (node creation, subscription setup, etc.). -* Running SQL scripts. -* Conditional logic or waiting for certain states. - -**Available Sample Workflows:** - -The `spockctrl/workflows/` directory in the source distribution contains several example workflows: - -* `add_node.json`: A workflow to add a new node to an existing Spock replication setup. This involves creating the node, setting up replication sets, and creating subscriptions. -* `remove_node.json`: A workflow to cleanly remove a node from a replication setup. -* `add_4th_node.json`: A workflow to add a 4th node to a 3 node cluster. - -These sample workflows can serve as templates or starting points for creating your own custom automation scripts. Examine their content to understand how they are structured and what operations they perform. - -**When to Use Workflows:** - -* **To Standardized Setups** Using a workflow ensures consistent configuration when adding new nodes or setting up replication. -* **Complex Operations** Using a workflow when performing complex operations ensures that your sequence of commands is successful; complex operations can be prone to manual error. -* **Disaster Recovery/Failover** Using a scripted procedure for failover or to re-configure replication after an outage is the best way to ensure that your recovery goes as planned. Please note that this use of workflows requires careful design! - - -### Example - Workflow to Add a Node to a Two-Node Cluster - -In this example, we'll walk through the stanzas that make up a workflow that adds a new node to a two-node cluster. - -Within the workflow file, the `COMMAND` property identifies the action performed by the stanza in which it is used. Spock 5.0 supports the following `COMMANDs`: - -| COMMAND | Description | -|---------|-------------| -| CREATE NODE | Add a node to a cluster. | -| DROP NODE | Drop a node from a cluster. | -| CREATE SUBSCRIPTION | Add a subscription to a cluster. | -| DROP SUBSCRIPTION| Drop a subscription from a cluster. | -| CREATE REPSET | Add a repset to a cluster. | -| DROP REPSET | Drop a node to a cluster. | -| CREATE SLOT | Add a replication slot. | -| DROP SLOT | Drop a replication slot. | -| ENABLE SUBSCRIPTION | Start replication on a node. | -| DISABLE SUBSCRIPTION | Stop replication on a node. | -| SQL | Invoke the specified Postgres SQL command. | - -Our sample workflow adds a third node to a new node cluster. The first stanza provides connection information for the host of the new node. Spockctrl can add only one node per workflow file; provide this information for each new node you add to your cluster. - - -In this walkthrough, we're using a two-node cluster; if your cluster is larger than two nodes, any actions performed on the replica node (in our example, `n2`) should be performed on *every* replica node in your cluster. A replica node is any existing node that is not used as a source node. - - -This stanza associates the name of the new node with the connection properties of the new node: - -* Provide the new node name in the `node` property and the `--node_name` property - the name must be identical. -* `--dsn` specifies the connection properties of the new node. - -```json -{ - "workflow_name": "Add Node", - "description": "Adding third node (n3) to two node (n1,n2) cluster.", - "steps": [ - { - "spock": { - "node": "n3", - "command": "CREATE NODE", - "description": "Create a spock node n3", - "args": [ - "--node_name=n3", - "--dsn=host=127.0.0.1 port=5433 user=my_username password=my_password", - "--location=Los Angeles", - "--country=USA", - "--info={\"key\": \"value\"}" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` -The next stanza creates the subscription from the new node (`n3`) to the source node (`n1`). In this stanza: - -* the `node` property specifies the name of the node in our original cluster that is used as our source node. The content of this node will be copied to the new node that we are creating. -* `--sub_name` specifies the name of the new subscription. -* `--provider_dsn` specifies the connection properties of the provider (our new node). -* `--replication_sets` specifies the names of the replication sets created for the subscription. -* `--enabled`, `--synchronize_data` and `--synchronize_structure` must be `true`. - -```json - { - "spock": { - "node": "n1", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n3_n1) on (n1) for n3->n1", - "sleep": 0, - "args": [ - "--sub_name=sub_n3_n1", - "--provider_dsn=host=127.0.0.1 port=5433 user=my_username password=my_password", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=true", - "--synchronize_data=true", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -Our next stanza creates subscriptions between the new node and any existing replica nodes. - -* `node` specifies the name of the existing replica node. -* `--provider_dsn` specifies the connection properties of the new node; this is the provider node for the new subscription. -* `--replication_sets` specifies the names of the replication sets created for the subscription -* `--enabled`, `--synchronize_data` and `--synchronize_structure` must be `true`. - - -You will need to include this stanza once for each replica node in your cluster; if your existing cluster has three nodes (one source node and two replica nodes), you will add two copies of this stanza. If your existing cluster has four nodes (one source node and three replica nodes), you will add three copies of this stanza. - - -```json - { - "spock": { - "node": "n2", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n3_n2) on (n2) for n3->n2", - "sleep": 0, - "args": [ - "--sub_name=sub_n3_n2", - "--provider_dsn=host=127.0.0.1 port=5433 user=my_username password=my_password", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=true", - "--synchronize_data=true", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "ignore_errors": false, - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next step, we wait for the apply worker to check state of the subscription. This step is optional, but should be considered a *best practice*. This step uses a SQL command to call the `spock.wait_for_apply_worker` function. - -* `$n2.sub_create` is a variable (the subscription ID) populated by the previous `CREATE SUBSCRIPTION` stanza. -* If needed, use the `sleep` property to accomodate processing time. - - -Note that if you have more than one replica node, and multiple `CREATE SUBSCRIPTION` stanzas, each stanza should be followed by a copy of this stanza, with the variable reset for each subsequent execution (`$n3.sub_create`, `$n4.sub_create`, etc). - - -```json - { - "sql": { - "node": "n2", - "command": "SQL", - "description": "Wait for apply worker on n2 subscription", - "sleep": 0, - "args": [ - "--sql=SELECT spock.wait_for_apply_worker($n2.sub_create, 1000);" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In our next stanza, we create a subscription between each replica node (`n2`) and our new node (`n3`). - -* `--provider_dsn` is the connection string of the replica node (in our example, the provider is the first node referenced in our subscription). -* `--enabled`, `--synchronize_data`, `--force_text_transfer` and `--synchronize_structure` must be `false`. - - -If you have multiple replica nodes, you will need one iteration of this stanza for each replica node in your cluster. - - -```json - { - "spock": { - "node": "n3", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n2_n3) on (n3) for n2->n3", - "sleep": 5, - "args": [ - "--sub_name=sub_n2_n3", - "--provider_dsn=host=127.0.0.1 port=5432 user=my_username password=my_password", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=false" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we use a `CREATE SLOT` command to create a replication slot for the new subscription between our existing replica (`n2`) and our new node (`n3`). Provide the slot name in the form `spk_database-name_node-name_subscription-name` where: - -* `database-name` is the name of your database. -* `node-name` name of the existing replica node. -* `subscription-name` is the subscription created in the previous step. - - -You must create one replication slot for each iteration of the CREATE SUBSCRIPTION stanza that allows a replica node to communicate with the new node; if you have three nodes in your cluster (one source node, and two replica nodes), you will need to provide one slot for each replica node (or two slots total). - - -```json - { - "spock": { - "node": "n2", - "command": "CREATE SLOT", - "description": "Create a logical replication slot spk_my_dbname_n2_sub_n2_n3 on (n2)", - "args": [ - "--slot=spk_my_dbname_n2_sub_n2_n3", - "--plugin=spock_output" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -Next, on each replica node, we invoke a SQL command that starts a sync event between the existing node(`n2`) and our source node (`n1`). This ensures that our replica nodes stay in sync with the source node for the duration of the ADD NODE process. - -The function (`spock.sync_event`) returns the log sequence number (LSN) of the sync event: - -```json - { - "sql": { - "node": "n2", - "command": "SQL", - "description": "Trigger a sync event on (n2)", - "sleep": 10, - "args": [ - "--sql=SELECT spock.sync_event();" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -The previous stanza returns the LSN of the sync event in the `$n2.sync_event` variable; in the next stanza, we watch for that variable so we know when the step is complete. - -* `node` specifies the cluster source node (in our example, `n1`). -* `$n2.sync_event` is the LSN returned by the previous stanza. -* If needed, you can use `sleep` to provide extra processing time for the step. - - -Include this stanza once for each iteration of the previous stanza within your workflow file. - - -```json - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Wait for a sync event on (n1) for n2-n1", - "sleep": 0, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n2', '$n2.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we create a subscription on our new target node (`n3`) from the source node (`n1`): - -* `arg->--sub_name` is `sub_n1_n3`. -* `arg->--provider_dsn` is the connection string for the source node (our provider, `n1`). -* `--enabled`, `--synchronize_data`, and `--synchronize_structure` must be `true`. -* `--force_text_transfer` must be `false`. - -```json - { - "spock": { - "node": "n3", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n1_n3) for n1 fpr n1->n3", - "sleep": 0, - "args": [ - "--sub_name=sub_n1_n3", - "--provider_dsn=host=127.0.0.1 port=5431 user=my_username password=my_password", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=true", - "--synchronize_data=true", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -Then, we include a stanza that triggers a sync event on the source node (`n1`) between the source node and the new node (`n3`). Use the `sleep` property to allocate time for the data to sync to the new node if needed. - -```json - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Trigger a sync event on (n1)", - "sleep": 5, - "args": [ - "--sql=SELECT spock.sync_event();" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we wait for the sync event started in the previous stanza to complete. Use the `sleep` property to allocate time for the data to sync to the new node if needed. - -```json - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Wait for a sync event on (n1) for n1-n3", - "sleep": 10, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n1', '$n1.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we check for data lag between the new node (n3) and any replica nodes in our cluster (n2). The timestamp returned is passed to the next stanza for use in evaluating the comparative state of the replica nodes and our new node. - -```json - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Check commit timestamp for n3 lag", - "sleep": 1, - "args": [ - "--sql=SELECT commit_timestamp FROM spock.lag_tracker WHERE origin_name = 'n2' AND receiver_name = 'n3'" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -In the next stanza, we use the timestamp from the previous stanza (`$n3.commit_timestamp`) to advance our replica slot to that location within our log files. This effectively advances transactions to the time specified (preventing duplicate entries from being written to the replica node). - -```json - { - "sql": { - "node": "n2", - "command": "SQL", - "description": "Advance the replication slot for n2->n3 based on a specific commit timestamp", - "sleep": 0, - "args": [ - "--sql=WITH lsn_cte AS (SELECT spock.get_lsn_from_commit_ts('spk_my_dbname_n2_sub_n2_n3', '$n3.commit_timestamp'::timestamp) AS lsn) SELECT pg_replication_slot_advance('spk_my_dbname_n2_sub_n2_n3', lsn) FROM lsn_cte;" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -Then, we enable the subscription from each replica node to the new node. At this point replication starts between any existing node and the new node (`n3`). - -```json - { - "spock": { - "node": "n3", - "command": "ENABLE SUBSCRIPTION", - "description": "Enable subscription (sub_n2_n3) on n3", - "args": [ - "--sub_name=sub_n2_n3", - "--immediate=true" - ], - "on_success": {}, - "on_failure": {} - } - }, -``` - -After starting replication, we check the lag time between the new node and each node in the cluster. This step invokes a SQL command that loops through each node in the cluster and compares the lag on each node until each returned value is comparable. Use the `sleep` property to extend processing time if needed. - -```json - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Advance the replication slot for n2->n3 based on a specific commit timestamp", - "sleep": 0, - "args": [ - "--sql=DO $$\nDECLARE\n lag_n1_n3 interval;\n lag_n2_n3 interval;\nBEGIN\n LOOP\n SELECT now() - commit_timestamp INTO lag_n1_n3\n FROM spock.lag_tracker\n WHERE origin_name = 'n1' AND receiver_name = 'n3';\n\n SELECT now() - commit_timestamp INTO lag_n2_n3\n FROM spock.lag_tracker\n WHERE origin_name = 'n2' AND receiver_name = 'n3';\n\n RAISE NOTICE 'n1 → n3 lag: %, n2 → n3 lag: %',\n COALESCE(lag_n1_n3::text, 'NULL'),\n COALESCE(lag_n2_n3::text, 'NULL');\n\n EXIT WHEN lag_n1_n3 IS NOT NULL AND lag_n2_n3 IS NOT NULL\n AND extract(epoch FROM lag_n1_n3) < 59\n AND extract(epoch FROM lag_n2_n3) < 59;\n\n PERFORM pg_sleep(1);\n END LOOP;\nEND\n$$;\n" - ], - "on_success": {}, - "on_failure": {} - } - } - ] -} -``` - -This tutorial provides a starting point for using `spockctrl`. For more advanced topics and troubleshooting, consult the output of `spockctrl --help` for specific commands and refer to any further documentation provided with the Spock replication system. - -The examples in this document should be considered illustrative. The exact commands, options, and workflow will depend on the specific version of `spockctrl` and the structure of its supporting configuration file. diff --git a/utils/spockctrl/build.sh b/utils/spockctrl/build.sh deleted file mode 100755 index 9f46fd98..00000000 --- a/utils/spockctrl/build.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash - -set -e - -echo "Detecting operating system..." -OS=$(uname -s) - -install_dependencies() { - case "$OS" in - Linux) - echo "Detecting Linux distribution..." - if [ -f /etc/debian_version ]; then - echo "Debian-based distribution detected." - sudo apt-get update - sudo apt-get install -y build-essential libssl-dev libjansson-dev perl libtest-harness-perl - sudo cpan IPC::Run - elif [ -f /etc/redhat-release ]; then - echo "Red Hat-based distribution detected." - sudo yum groupinstall -y "Development Tools" - sudo yum install -y openssl-devel jansson-devel perl-Test-Harness - sudo cpan IPC::Run - else - echo "Unsupported Linux distribution." - exit 1 - fi - ;; - Darwin) - echo "Installing dependencies for macOS..." - brew update - brew install openssl jansson perl - cpan Test::Harness - cpan IPC::Run - ;; - *) - echo "Unsupported operating system: $OS" - exit 1 - ;; - esac -} - -configure_environment() { - echo "Configuring environment..." -} - -build_project() { - echo "Building the project..." - echo "Build completed successfully." -} - -install_dependencies -configure_environment -build_project \ No newline at end of file diff --git a/utils/spockctrl/include/conf.h b/utils/spockctrl/include/conf.h deleted file mode 100644 index 8898d42a..00000000 --- a/utils/spockctrl/include/conf.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef CONF_H -#define CONF_H - -typedef struct -{ - char *cluster_name; - char *version; -} RaledConfig; - -typedef struct -{ - char *log_level; - char *log_destination; - char *log_file; -} LogConfig; - -typedef struct -{ - char *postgres_ip; - int postgres_port; - char *postgres_user; - char *postgres_password; - char *postgres_db; -} PostgresConfig; - -typedef struct -{ - char *node_name; - PostgresConfig postgres; -} SpockNodeConfig; - -typedef struct -{ - RaledConfig raled; - LogConfig log; - SpockNodeConfig *spock_nodes; - int spock_node_count; -} Config; - -/* Function to parse the JSON configuration file */ -int load_config(const char *filename); -void free_config(void); - -/* Functions to get values for each node using node_name */ -const char *get_postgres_ip(const char *node_name); -int get_postgres_port(const char *node_name); -const char *get_postgres_user(const char *node_name); -const char *get_postgres_password(const char *node_name); -const char *get_postgres_db(const char *node_name); - -/* Added getter */ -const char *get_postgres_coninfo(const char *node_name); - -#endif /* CONF_H */ diff --git a/utils/spockctrl/include/dbconn.h b/utils/spockctrl/include/dbconn.h deleted file mode 100644 index 3d15e39d..00000000 --- a/utils/spockctrl/include/dbconn.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef DBCONN_H -#define DBCONN_H - -#include - -/* Connect to the database */ -PGconn *connectdb(const char *conninfo); - -/* Disconnect from the database */ -void disconnectdb(PGconn *conn); - -/* Run an SQL command */ -int run_sql(PGconn *conn, const char *sql); - -/* Run an SQL query and process the results with a callback function */ -int run_sql_query(PGconn *conn, const char *sql, void (*cb) (PGresult *, int, int)); - -/* Get an integer value from the result set */ -int pg_getint(PGresult *res, int row, int col); - -/* Get a string value from the result set */ -const char *pg_getstring(PGresult *res, int row, int col); - -/* Get a boolean value from the result set */ -int pg_getbool(PGresult *res, int row, int col); - -#endif /* DBCONN_H */ diff --git a/utils/spockctrl/include/logger.h b/utils/spockctrl/include/logger.h deleted file mode 100644 index 835a169f..00000000 --- a/utils/spockctrl/include/logger.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef LOGGER_H -#define LOGGER_H - -#include -#include -#include -#include -#include -#include "util.h" - -#define COLOR_GREEN "\033[0;32m" -#define COLOR_RED "\033[0;31m" -#define COLOR_RESET "\033[0m" -#define COLOR_YELLOW "\033[0;33m" -#define COLOR_BLUE "\033[0;34m" -#define COLOR_DEFAULT "\033[0m" - -typedef enum -{ - LOG_LEVEL_NONE, - LOG_LEVEL_WARNING, - LOG_LEVEL_INFO, - LOG_LEVEL_ERROR, - LOG_LEVEL_DEBUG0, - LOG_LEVEL_DEBUG1 -} LogLevel; - -void log_message(const char *color, const char *symbol, const char *format, va_list args); -void log_info(const char *format,...); -void log_msg(const char *fmt,...); -void log_error(const char *fmt,...); -void log_warning(const char *fmt,...); -void log_debug0(const char *fmt,...); -void log_debug1(const char *fmt,...); - -extern LogLevel current_log_level; - -#endif /* // LOGGER_H */ diff --git a/utils/spockctrl/include/node.h b/utils/spockctrl/include/node.h deleted file mode 100644 index 52368f2d..00000000 --- a/utils/spockctrl/include/node.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef NODE_H -#define NODE_H - -void print_node_help(void); -int handle_node_command(int argc, char *argv[]); - -int handle_node_create_command(int argc, char *argv[]); -int handle_node_drop_command(int argc, char *argv[]); -int handle_node_add_interface_command(int argc, char *argv[]); -int handle_node_drop_interface_command(int argc, char *argv[]); - - -#endif /* NODE_H */ diff --git a/utils/spockctrl/include/repset.h b/utils/spockctrl/include/repset.h deleted file mode 100644 index ea8290b1..00000000 --- a/utils/spockctrl/include/repset.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef REPSET_H -#define REPSET_H - -void print_repset_help(void); -int handle_repset_command(int argc, char *argv[]); - -int handle_repset_create_command(int argc, char *argv[]); -int handle_repset_alter_command(int argc, char *argv[]); -int handle_repset_drop_command(int argc, char *argv[]); -int handle_repset_add_table_command(int argc, char *argv[]); -int handle_repset_remove_table_command(int argc, char *argv[]); -int handle_repset_add_partition_command(int argc, char *argv[]); -int handle_repset_remove_partition_command(int argc, char *argv[]); -int handle_repset_list_tables_command(int argc, char *argv[]); - - -#endif /* // REPSET_H */ diff --git a/utils/spockctrl/include/slot.h b/utils/spockctrl/include/slot.h deleted file mode 100644 index dfeb9aa4..00000000 --- a/utils/spockctrl/include/slot.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef SLOT_H -#define SLOT_H - -/* Handle slot command-line subcommands */ -int handle_slot_command(int argc, char *argv[]); - -/* Print slot command help */ -void print_slot_help(void); - - -int handle_slot_create_command(int argc, char *argv[]); -int handle_slot_drop_command(int argc, char *argv[]); -int handle_slot_enable_command(int argc, char *argv[]); -int handle_slot_disable_command(int argc, char *argv[]); - -#endif /* // SLOT_H */ diff --git a/utils/spockctrl/include/spock.h b/utils/spockctrl/include/spock.h deleted file mode 100644 index 0f09808b..00000000 --- a/utils/spockctrl/include/spock.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef SPOCK_H -#define SPOCK_H - -#include -#include -#include -#include -#include -#include "dbconn.h" -#include "conf.h" -#include "logger.h" - -extern Config config; - -int call_wait_for_sync_event(PGconn *conn, const char *origin_type, const char *origin, const char *lsn, int timeout); -int handle_spock_wait_for_sync_event_command(int argc, char *argv[]); -int handle_spock_sync_event_command(int argc, char *argv[]); - -#endif /* // SPOCK_H */ diff --git a/utils/spockctrl/include/spockctrl.h b/utils/spockctrl/include/spockctrl.h deleted file mode 100644 index f8ea3d44..00000000 --- a/utils/spockctrl/include/spockctrl.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef SPOCKCTRL_H -#define SPOCKCTRL_H - - -#endif /* SPOCKCTRL_H */ diff --git a/utils/spockctrl/include/sql.h b/utils/spockctrl/include/sql.h deleted file mode 100644 index 840b47c3..00000000 --- a/utils/spockctrl/include/sql.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef SPOCKCTRL_SQL_H -#define SPOCKCTRL_SQL_H - -int handle_sql_exec_command(int argc, char *argv[]); -void print_sql_help(void); -void print_sql_exec_help(void); - -#endif /* SPOCKCTRL_SQL_H */ diff --git a/utils/spockctrl/include/sub.h b/utils/spockctrl/include/sub.h deleted file mode 100644 index 4df46cd0..00000000 --- a/utils/spockctrl/include/sub.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef SUB_H -#define SUB_H - -void print_sub_help(void); -int handle_sub_command(int argc, char *argv[]); - - -/* Function declarations */ -int handle_sub_create_command(int argc, char *argv[]); -int handle_sub_drop_command(int argc, char *argv[]); -int handle_sub_enable_command(int argc, char *argv[]); -int handle_sub_disable_command(int argc, char *argv[]); -int handle_sub_show_status_command(int argc, char *argv[]); -int handle_sub_show_table_command(int argc, char *argv[]); -int handle_sub_resync_table_command(int argc, char *argv[]); -int handle_sub_add_repset_command(int argc, char *argv[]); -int handle_sub_remove_repset_command(int argc, char *argv[]); - -#endif /* // SUB_H */ diff --git a/utils/spockctrl/include/util.h b/utils/spockctrl/include/util.h deleted file mode 100644 index c3155280..00000000 --- a/utils/spockctrl/include/util.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef UTIL_H -#define UTIL_H - -#include -#include - -/* JSON utility functions */ -int is_valid_json(const char *json_str); -json_t *load_json_file(const char *file_path); -json_t *parse_json_string(const char *json_str); -char *get_json_string_value(json_t * json, const char *key); -json_t *get_json_array(json_t * json, const char *key); - -/* Function to get the current timestamp */ -char *get_current_timestamp(void); - -/* Function to get the current user */ -char *get_current_user(void); - -/* Function to trim whitespace from a string */ -char *trim_whitespace(char *str); - -/* Function to convert a string to lowercase */ -char *str_to_lower(char *str); - -/* Function to convert a string to uppercase */ -char *str_to_upper(char *str); - -/* Function to create a formatted SQL string */ -char *make_sql(const char *format,...); - -/* Function to create a PostgreSQL SELECT query */ -char *make_select_query(const char *table, const char *columns, const char *condition); - -/* Function to create a Spock-specific query with variable parameters */ -char *make_spock_query(const char *command, const char *params_format,...); - -/* Function to create a Spock-specific query with variable parameters */ -void trim_newline(char *str); - -/* Function to substitute SQL variables with values from node.out */ -int get_value_from_outfile(const char *node, const char *key, char *value, size_t value_sz); - -/* Function to substitute SQL variables in a statement */ -char *substitute_sql_vars(const char *sql_stmt); - -#endif /* UTIL_H */ diff --git a/utils/spockctrl/include/workflow.h b/utils/spockctrl/include/workflow.h deleted file mode 100644 index fd9fc0d1..00000000 --- a/utils/spockctrl/include/workflow.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef WORKFLOW_H -#define WORKFLOW_H - -#include -#include - -#define MAX_ARGS 15 -#define MAX_OPTIONS 10 - -/* Step type constants */ -#define STEP_TYPE_SPOCK 1 -#define STEP_TYPE_SQL 2 -#define STEP_TYPE_SHELL 3 - -typedef struct Step -{ - char *name; /* Name of the step */ - char *node; /* Node associated with the step */ - char *description; /* Description of the step */ - char *command; /* Command to execute */ - char *args[MAX_ARGS]; /* Arguments for the command */ - char *options[MAX_OPTIONS]; /* Options for the command */ - char *on_success; /* JSON string for on_success actions */ - char *on_failure; /* JSON string for on_failure actions */ - int sleep; /* Sleep time after the step */ - int type; /* Type of the step (e.g., STEP_TYPE_SPOCK, - * STEP_TYPE_SQL, STEP_TYPE_SHELL) */ - bool ignore_errors; /* Ignore errors for this step */ -} Step; - -typedef struct Workflow -{ - char *workflow_name; /* Name of the workflow */ - char *description; /* Description of the workflow */ - Step *steps; /* Array of steps in the workflow */ - int step_count; /* Number of steps in the workflow */ - Step success_step; /* Step to execute on success */ - Step failure_step; /* Step to execute on failure */ -} Workflow; - -/* Function declarations */ -Workflow *load_workflow(const char *json_file_path); -void free_workflow(Workflow *workflow); -int run_workflow(Workflow *workflow); - -#endif /* WORKFLOW_H */ diff --git a/utils/spockctrl/spockctrl.json b/utils/spockctrl/spockctrl.json deleted file mode 100644 index b236c107..00000000 --- a/utils/spockctrl/spockctrl.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "global": { - "spock": { - "cluster_name": "pgedge", - "version": "1.0.0" - }, - "log": { - "log_level": "INFO", - "log_destination": "console", - "log_file": "/var/log/spockctrl.log" - } - }, - "spock-nodes": [ - { - "node_name": "n1", - "postgres": { - "postgres_ip": "127.0.0.1", - "postgres_port": 5431, - "postgres_user": "pgedge", - "postgres_password": "pgedge", - "postgres_db": "pgedge" - } - }, - { - "node_name": "n2", - "postgres": { - "postgres_ip": "127.0.0.1", - "postgres_port": 5432, - "postgres_user": "pgedge", - "postgres_password": "pgedge", - "postgres_db": "pgedge" - } - }, - { - "node_name": "n3", - "postgres": { - "postgres_ip": "127.0.0.1", - "postgres_port": 5433, - "postgres_user": "pgedge", - "postgres_password": "pgedge", - "postgres_db": "pgedge" - } - } - ] -} diff --git a/utils/spockctrl/src/conf.c b/utils/spockctrl/src/conf.c deleted file mode 100644 index f90942a3..00000000 --- a/utils/spockctrl/src/conf.c +++ /dev/null @@ -1,353 +0,0 @@ -/*------------------------------------------------------------------------- - * - * conf.c - * configuration file parsing and access functions - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include -#include -#include -#include -#include -#include "conf.h" -#include "logger.h" - -static Config config; - -/* Function to parse the JSON configuration file */ -int -load_config(const char *filename) -{ - json_t *root = NULL; - json_error_t error; - json_t *global = NULL; - json_t *raled = NULL; - json_t *log = NULL; - json_t *spock_nodes = NULL; - json_t *postgres; - - /* Load the JSON file */ - root = json_load_file(filename, 0, &error); - if (!root) - { - log_error("Error loading JSON file: %s", error.text); - return -1; - } - - /* Parse global.raled */ - global = json_object_get(root, "global"); - if (!global) - { - log_error("Error: global object not found in JSON file"); - goto exit_err; - } - - raled = json_object_get(global, "spock"); - if (!raled) - { - log_error("Error: spock object not found in global"); - goto exit_err; - } - - - config.raled.cluster_name = strdup(json_string_value(json_object_get(raled, "cluster_name"))); - if (!config.raled.cluster_name) - { - log_error("Error: memory allocation failed for cluster_name"); - goto exit_err; - } - - config.raled.version = strdup(json_string_value(json_object_get(raled, "version"))); - if (!config.raled.version) - { - log_error("Error: memory allocation failed for version"); - goto exit_err; - } - - /* Parse global.log */ - log = json_object_get(global, "log"); - if (!log) - { - log_error("Error: log object not found in global"); - goto exit_err; - } - - config.log.log_level = strdup(json_string_value(json_object_get(log, "log_level"))); - if (!config.log.log_level) - { - log_error("Error: memory allocation failed for log_level"); - goto exit_err; - } - - config.log.log_destination = strdup(json_string_value(json_object_get(log, "log_destination"))); - if (!config.log.log_destination) - { - log_error("Error: memory allocation failed for log_destination"); - goto exit_err; - } - - config.log.log_file = strdup(json_string_value(json_object_get(log, "log_file"))); - if (!config.log.log_file) - { - log_error("Error: memory allocation failed for log_file"); - goto exit_err; - } - - /* Parse spock-nodes */ - spock_nodes = json_object_get(root, "spock-nodes"); - if (!spock_nodes) - { - log_error("Error: spock-nodes array not found in JSON file"); - goto exit_err; - } - - config.spock_node_count = json_array_size(spock_nodes); - config.spock_nodes = malloc(config.spock_node_count * sizeof(SpockNodeConfig)); - if (!config.spock_nodes) - { - log_error("Error: memory allocation failed for spock_nodes"); - goto exit_err; - } - - for (int i = 0; i < config.spock_node_count; i++) - { - json_t *node = json_array_get(spock_nodes, i); - config.spock_nodes[i].node_name = strdup(json_string_value(json_object_get(node, "node_name"))); - if (!config.spock_nodes[i].node_name) - { - log_error("Error: memory allocation failed for node_name"); - goto exit_err; - } - - postgres = json_object_get(node, "postgres"); - if (!postgres) - { - log_error("Error: postgres object not found in node"); - goto exit_err; - } - - config.spock_nodes[i].postgres.postgres_ip = strdup(json_string_value(json_object_get(postgres, "postgres_ip"))); - if (!config.spock_nodes[i].postgres.postgres_ip) - { - log_error("Error: memory allocation failed for postgres_ip"); - goto exit_err; - } - - config.spock_nodes[i].postgres.postgres_port = json_integer_value(json_object_get(postgres, "postgres_port")); - - config.spock_nodes[i].postgres.postgres_user = strdup(json_string_value(json_object_get(postgres, "postgres_user"))); - if (!config.spock_nodes[i].postgres.postgres_user) - { - log_error("Error: memory allocation failed for postgres_user"); - goto exit_err; - } - - config.spock_nodes[i].postgres.postgres_password = strdup(json_string_value(json_object_get(postgres, "postgres_password"))); - if (!config.spock_nodes[i].postgres.postgres_password) - { - log_error("Error: memory allocation failed for postgres_password"); - goto exit_err; - } - - // Parse postgres_db - config.spock_nodes[i].postgres.postgres_db = strdup(json_string_value(json_object_get(postgres, "postgres_db"))); - if (!config.spock_nodes[i].postgres.postgres_db) - { - log_error("Error: memory allocation failed for postgres_db"); - goto exit_err; - } - } - - /* Free the JSON root object */ - json_decref(root); - return 0; - -exit_err: - - if (config.raled.cluster_name) free(config.raled.cluster_name); - if (config.raled.version) free(config.raled.version); - if (config.log.log_level) free(config.log.log_level); - if (config.log.log_destination) free(config.log.log_destination); - if (config.log.log_file) free(config.log.log_file); - - if (config.spock_nodes) - { - for (int i = 0; i < config.spock_node_count; i++) - { - if (config.spock_nodes[i].node_name) free(config.spock_nodes[i].node_name); - if (config.spock_nodes[i].postgres.postgres_ip) free(config.spock_nodes[i].postgres.postgres_ip); - if (config.spock_nodes[i].postgres.postgres_user) free(config.spock_nodes[i].postgres.postgres_user); - if (config.spock_nodes[i].postgres.postgres_password) free(config.spock_nodes[i].postgres.postgres_password); - if (config.spock_nodes[i].postgres.postgres_db) free(config.spock_nodes[i].postgres.postgres_db); - } - free(config.spock_nodes); - } - - if (root) json_decref(root); - - return -1; -} - -/* Function to free the allocated memory for the configuration */ -void -free_config(void) -{ - free(config.raled.cluster_name); - free(config.raled.version); - free(config.log.log_level); - free(config.log.log_destination); - free(config.log.log_file); - - for (int i = 0; i < config.spock_node_count; i++) - { - free(config.spock_nodes[i].node_name); - free(config.spock_nodes[i].postgres.postgres_ip); - free(config.spock_nodes[i].postgres.postgres_user); - free(config.spock_nodes[i].postgres.postgres_password); - free(config.spock_nodes[i].postgres.postgres_db); - } - - free(config.spock_nodes); -} - -/* Function to get the PostgreSQL IP for a given node name */ -const char * -get_postgres_ip(const char *node_name) -{ - for (int i = 0; i < config.spock_node_count; i++) - { - if (strcmp(config.spock_nodes[i].node_name, node_name) == 0) - { - return config.spock_nodes[i].postgres.postgres_ip; - } - } - return NULL; -} - -/* Function to get the PostgreSQL port for a given node name */ -int -get_postgres_port(const char *node_name) -{ - for (int i = 0; i < config.spock_node_count; i++) - { - if (strcmp(config.spock_nodes[i].node_name, node_name) == 0) - { - return config.spock_nodes[i].postgres.postgres_port; - } - } - return -1; -} - -/* Function to get the PostgreSQL user for a given node name */ -const char * -get_postgres_user(const char *node_name) -{ - for (int i = 0; i < config.spock_node_count; i++) - { - if (strcmp(config.spock_nodes[i].node_name, node_name) == 0) - { - return config.spock_nodes[i].postgres.postgres_user; - } - } - return NULL; -} - -/* Function to get the PostgreSQL password for a given node name */ -const char * -get_postgres_password(const char *node_name) -{ - for (int i = 0; i < config.spock_node_count; i++) - { - if (strcmp(config.spock_nodes[i].node_name, node_name) == 0) - { - return config.spock_nodes[i].postgres.postgres_password; - } - } - return NULL; -} - -/* Function to get the PostgreSQL db for a given node name */ -const char * -get_postgres_db(const char *node_name) -{ - for (int i = 0; i < config.spock_node_count; i++) - { - if (strcmp(config.spock_nodes[i].node_name, node_name) == 0) - { - return config.spock_nodes[i].postgres.postgres_db; - } - } - return NULL; -} - -const char * -get_postgres_coninfo(const char *node_name) -{ - bool found = false; - const char *ip; - int port; - const char *user; - const char *password; - const char *db; - char *coninfo; - - for (int i = 0; i < config.spock_node_count; i++) - { - if (strcmp(config.spock_nodes[i].node_name, node_name) == 0) - { - found = true; - } - } - if (!found) - { - log_error("Error: node '%s' not found in configuration", node_name); - return NULL; - } - - ip = get_postgres_ip(node_name); - port = get_postgres_port(node_name); - user = get_postgres_user(node_name); - password = get_postgres_password(node_name); - db = get_postgres_db(node_name); - - if (!ip) - { - log_error("Error: IP address not found for node '%s'", node_name); - return NULL; - } - if (port == -1) - { - log_error("Error: Port not found for node '%s'", node_name); - return NULL; - } - if (!user) - { - log_error("Error: User not found for node '%s'", node_name); - return NULL; - } - if (!password) - { - log_error("Error: Password not found for node '%s'", node_name); - return NULL; - } - if (!db) - { - log_error("Error: Database not found for node '%s'", node_name); - return NULL; - } - - coninfo = malloc(256); - if (!coninfo) - { - log_error("Error: memory allocation failed for connection info"); - return NULL; - } - snprintf(coninfo, 256, "host=%s port=%d user=%s password=%s dbname=%s", ip, port, user, password, db); - return coninfo; -} \ No newline at end of file diff --git a/utils/spockctrl/src/dbconn.c b/utils/spockctrl/src/dbconn.c deleted file mode 100644 index cdb6bb6c..00000000 --- a/utils/spockctrl/src/dbconn.c +++ /dev/null @@ -1,129 +0,0 @@ -/*------------------------------------------------------------------------- - * - * dbconn.c - * database connection and utility functions - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include -#include -#include -#include "logger.h" -#include "util.h" -#include "dbconn.h" - -/* Connect to the database */ -PGconn * -connectdb(const char *conninfo) -{ - PGconn *conn; - - /* Make a connection to the database */ - conn = PQconnectdb(conninfo); - - /* Check to see that the backend connection was successfully made */ - if (PQstatus(conn) != CONNECTION_OK) - { - log_error("Connection to database failed\n %s", PQerrorMessage(conn)); - PQfinish(conn); - return NULL; - } - - return conn; -} - -/* Disconnect from the database */ -void -disconnectdb(PGconn *conn) -{ - PQfinish(conn); -} - -/* Run an SQL command */ -int -run_sql(PGconn *conn, const char *sql) -{ - PGresult *res; - - res = PQexec(conn, sql); - - /* Check for successful command execution */ - if (PQresultStatus(res) != PGRES_COMMAND_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - return 1; - } - - /* Clear result */ - PQclear(res); - return 0; -} - -/* Run an SQL query and process the results with a callback function */ -int -run_sql_query(PGconn *conn, const char *sql, void (*cb)(PGresult *, int, int)) -{ - PGresult *res; - int rows; - int cols; - - res = PQexec(conn, sql); - - /* Check for successful command execution */ - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SELECT command did not return tuples properly: %s", PQerrorMessage(conn)); - PQclear(res); - return 1; - } - - /* Get the number of rows and columns */ - rows = PQntuples(res); - cols = PQnfields(res); - - /* Call the callback function */ - cb(res, rows, cols); - - /* Clear result */ - PQclear(res); - return 0; -} - -/* Get an integer value from the result set */ -int -pg_getint(PGresult *res, int row, int col) -{ - if (PQgetisnull(res, row, col)) - { - return 0; - } - return atoi(PQgetvalue(res, row, col)); -} - -/* Get a string value from the result set */ -const char * -pg_getstring(PGresult *res, int row, int col) -{ - if (PQgetisnull(res, row, col)) - { - return NULL; - } - return PQgetvalue(res, row, col); -} - -/* Get a boolean value from the result set */ -int -pg_getbool(PGresult *res, int row, int col) -{ - if (PQgetisnull(res, row, col)) - { - return 0; - } - return (PQgetvalue(res, row, col)[0] == 't'); -} \ No newline at end of file diff --git a/utils/spockctrl/src/logger.c b/utils/spockctrl/src/logger.c deleted file mode 100644 index a0fd1d0c..00000000 --- a/utils/spockctrl/src/logger.c +++ /dev/null @@ -1,99 +0,0 @@ -/*------------------------------------------------------------------------- - * - * logger.c - * logging utility functions - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include "logger.h" -#include "util.h" -#include -#include -#include - -LogLevel current_log_level = LOG_LEVEL_NONE; - -void log_message(const char *color, const char *symbol, const char *format, va_list args) -{ - time_t now; - struct tm *local_time; - char time_buffer[20]; - - now = time(NULL); - local_time = localtime(&now); - strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d %H:%M:%S", local_time); - - fprintf(stderr, "%s[%s] %s", color, time_buffer, symbol); - vfprintf(stderr, format, args); - fprintf(stderr, "%s\n", COLOR_RESET); -} - -void log_info(const char *format, ...) -{ - va_list args; - - if (current_log_level >= LOG_LEVEL_INFO) - { - va_start(args, format); - log_message(COLOR_GREEN, "[INFO] ", format, args); - va_end(args); - } -} -void log_msg(const char *fmt, ...) -{ - va_list args; - - va_start(args, fmt); - log_message(COLOR_DEFAULT, "", fmt, args); - va_end(args); -} - -void log_error(const char *fmt, ...) -{ - va_list args; - - va_start(args, fmt); - log_message(COLOR_RED, "[ERROR]: ", fmt, args); - va_end(args); -} - -void log_warning(const char *fmt, ...) -{ - va_list args; - - if (current_log_level >= LOG_LEVEL_WARNING) - { - va_start(args, fmt); - log_message(COLOR_YELLOW, "[WARN] ", fmt, args); - va_end(args); - } -} - -void log_debug0(const char *fmt, ...) -{ - va_list args; - - if (current_log_level >= LOG_LEVEL_DEBUG0) - { - va_start(args, fmt); - log_message(COLOR_BLUE, "[DEBUG]", fmt, args); - va_end(args); - } -} - -void log_debug1(const char *fmt, ...) -{ - va_list args; - - if (current_log_level >= LOG_LEVEL_DEBUG1) - { - va_start(args, fmt); - log_message(COLOR_BLUE, "[DEBUG]", fmt, args); - va_end(args); - } -} \ No newline at end of file diff --git a/utils/spockctrl/src/node.c b/utils/spockctrl/src/node.c deleted file mode 100644 index 0763ddae..00000000 --- a/utils/spockctrl/src/node.c +++ /dev/null @@ -1,947 +0,0 @@ -/*------------------------------------------------------------------------- - * - * node.c - * node management and command handling functions - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include -#include -#include -#include -#include "dbconn.h" -#include "node.h" -#include "logger.h" -#include "util.h" -#include "conf.h" - -/* Function declarations */ -static int node_spock_version(int argc, char *argv[]); -static int node_pg_version(int argc, char *argv[]); -static int node_status(int argc, char *argv[]); -static int node_gucs(int argc, char *argv[]); - -static void print_node_spock_version_help(void); -static void print_node_pg_version_help(void); -static void print_node_status_help(void); -static void print_node_gucs_help(void); -static void print_node_create_help(void); -static void print_node_drop_help(void); -static void print_node_add_interface_help(void); -static void print_node_drop_interface_help(void); - -int get_pg_version(const char *conninfo, char *pg_version); - -extern int verbose; - -void -print_node_help(void) -{ - printf("Usage: spockctrl node [options]\n"); - printf("Subcommands:\n"); - printf(" spock-version Show Spock version\n"); - printf(" pg-version Show PostgreSQL version\n"); - printf(" status Show node status\n"); - printf(" gucs Show node GUCs\n"); - printf(" create Create a node\n"); - printf(" drop Drop a node\n"); - printf(" add-interface Add an interface to a node\n"); - printf(" drop-interface Drop an interface from a node\n"); -} - -int -handle_node_command(int argc, char *argv[]) -{ - int i; - struct - { - const char *cmd; - int (*func) (int, char *[]); - } commands[] = { - {"spock-version", node_spock_version}, - {"pg-version", node_pg_version}, - {"status", node_status}, - {"gucs", node_gucs}, - {"create", handle_node_create_command}, - {"drop", handle_node_drop_command}, - {"add-interface", handle_node_add_interface_command}, - {"drop-interface", handle_node_drop_interface_command}, - }; - - if (argc < 2) - { - log_error("No subcommand provided for node."); - print_node_help(); - return EXIT_FAILURE; - } - - if (strcmp(argv[0], "--help") == 0) - { - print_node_help(); - return EXIT_SUCCESS; - } - - for (i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) - { - if (strcmp(argv[1], commands[i].cmd) == 0) - { - commands[i].func(argc, &argv[0]); - return EXIT_SUCCESS; - } - } - - log_error("Unknown subcommand for node."); - print_node_help(); - return EXIT_FAILURE; -} - -int -node_spock_version(int argc, char *argv[]) -{ - int option_index = 0; - int c; - const char *conninfo = NULL; - char *node = NULL; - - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"config", required_argument, 0, 'c'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - optind = 1; - while ((c = getopt_long(argc, argv, "n:h:c", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 'c': - break; - case 'h': - print_node_spock_version_help(); - return EXIT_SUCCESS; - default: - print_node_spock_version_help(); - return EXIT_FAILURE; - } - } - - if (node == NULL) - { - log_error("Node name is required."); - print_node_spock_version_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - return EXIT_SUCCESS; -} - -int -node_pg_version(int argc, char *argv[]) -{ - int option_index = 0; - int c; - const char *conninfo = NULL; - const char *node = NULL; - char pg_version[256]; - - static struct option long_options[] = { - {"node", required_argument, 0, 't'}, - {"config", required_argument, 0, 'c'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - optind = 1; - while ((c = getopt_long(argc, argv, "t:h:c", long_options, &option_index)) != -1) - { - switch (c) - { - case 't': - node = optarg; - break; - case 'h': - print_node_pg_version_help(); - return EXIT_SUCCESS; - case 'c': - break; - default: - print_node_pg_version_help(); - return EXIT_FAILURE; - } - } - if (node == NULL) - { - log_error("Node name is required."); - print_node_pg_version_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - if (get_pg_version(conninfo, pg_version) != 0) - { - log_error("Failed to get PostgreSQL version for node %s", node); - return EXIT_FAILURE; - } - printf("%s\n", pg_version); - return EXIT_SUCCESS; -} - -int -node_status(int argc, char *argv[]) -{ - int option_index = 0; - int c; - const char *conninfo = NULL; - const char *node = NULL; - - static struct option long_options[] = { - {"node", required_argument, 0, 't'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - optind = 1; - while ((c = getopt_long(argc, argv, "t:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 't': - node = optarg; - break; - case 'h': - print_node_status_help(); - return EXIT_SUCCESS; - default: - print_node_status_help(); - return EXIT_FAILURE; - } - } - - if (node == NULL) - { - log_error("Node name is required."); - print_node_status_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - return EXIT_SUCCESS; -} - -int -node_gucs(int argc, char *argv[]) -{ - int option_index = 0; - int c; - const char *conninfo = NULL; - const char *node = NULL; - - static struct option long_options[] = { - {"node", required_argument, 0, 't'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - optind = 1; - while ((c = getopt_long(argc, argv, "t:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 't': - node = optarg; - break; - case 'h': - print_node_gucs_help(); - return EXIT_SUCCESS; - default: - print_node_gucs_help(); - return EXIT_FAILURE; - } - } - - if (node == NULL) - { - log_error("Node name is required."); - print_node_gucs_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - return EXIT_SUCCESS; -} - -int -handle_node_create_command(int argc, char *argv[]) -{ - int option_index = 0; - int c; - const char *conninfo = NULL; - char *node_name = NULL; - char *node = NULL; - char *dsn = NULL; - char *location = NULL; - char *country = NULL; - char *info = NULL; - PGconn *conn = NULL; - PGresult *res = NULL; - char sql[4096]; - char location_val[512]; - char country_val[512]; - char info_val[1050]; - - static struct option long_options[] = { - {"node", required_argument, 0, 't'}, - {"node_name", required_argument, 0, 'n'}, - {"dsn", required_argument, 0, 'd'}, - {"location", optional_argument, 0, 'l'}, - {"country", optional_argument, 0, 'c'}, - {"info", optional_argument, 0, 'i'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - optind = 1; - /* Parse command-line arguments */ - while ((c = getopt_long(argc, argv, "t:n:d:l:c:i:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 't': - node = optarg; - break; - case 'n': - node_name = optarg; - break; - case 'd': - dsn = optarg; - break; - case 'l': - location = optarg; - break; - case 'c': - country = optarg; - break; - case 'i': - info = optarg; - break; - case 'h': - print_node_create_help(); - return EXIT_SUCCESS; - default: - log_error("Invalid option provided."); - print_node_create_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node) - { - log_error("Missing required arguments: --node are mandatory."); - print_node_create_help(); - return EXIT_FAILURE; - } - - /* Validate required arguments */ - if (!node_name || !dsn) - { - log_error("Missing required arguments: --node_name and --dsn are mandatory."); - print_node_create_help(); - return EXIT_FAILURE; - } - - /* Validate optional arguments */ - if (info && strlen(info) > 1024) - { - log_error("The --info parameter is too long. Maximum length is 1024 characters."); - return EXIT_FAILURE; - } - - /* Reformat the --info parameter if necessary */ - if (info && !is_valid_json(info)) - { - log_error("The --info parameter is not valid JSON: '%s'", info); - return EXIT_FAILURE; - } - - - if (info && !is_valid_json(info)) - { - log_error("The --info parameter is not valid JSON: '%s'", info); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node_name); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Prepare quoted/NULL values for SQL */ - - if (location) - snprintf(location_val, sizeof(location_val), "'%s'", location); - else - snprintf(location_val, sizeof(location_val), "NULL"); - - if (country) - snprintf(country_val, sizeof(country_val), "'%s'", country); - else - snprintf(country_val, sizeof(country_val), "NULL"); - - if (info) - snprintf(info_val, sizeof(info_val), "'%s'", info); - else - snprintf(info_val, sizeof(info_val), "NULL"); - - snprintf(sql, sizeof(sql), - "SELECT spock.node_create(" - "node_name := '%s', " - "dsn := '%s', " - "location := %s, " - "country := %s, " - "info := %s::jsonb);", - node_name, - dsn, - location_val, - country_val, - info_val); - - log_debug0("SQL: %s", sql); - - /* Execute SQL query */ - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Check for NULL result */ - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Clean up */ - PQclear(res); - PQfinish(conn); - - return EXIT_SUCCESS; -} - -int -handle_node_drop_command(int argc, char *argv[]) -{ - int option_index = 0; - int c; - const char *conninfo = NULL; - char *node_name = NULL; - char *node = NULL; - int ifexists = 0; - PGconn *conn = NULL; - PGresult *res = NULL; - char sql[1024]; - - static struct option long_options[] = { - {"node", required_argument, 0, 't'}, - {"node_name", required_argument, 0, 'n'}, - {"ifexists", no_argument, 0, 'e'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - optind = 1; - - /* Parse command-line arguments */ - while ((c = getopt_long(argc, argv, "t:n:eh", long_options, &option_index)) != -1) - { - switch (c) - { - case 't': - node = optarg; - break; - case 'n': - node_name = optarg; - break; - - case 'e': - ifexists = 1; - break; - case 'h': - print_node_drop_help(); - return EXIT_SUCCESS; - default: - print_node_drop_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node) - { - log_error("Missing required argument: --node is mandatory."); - print_node_drop_help(); - return EXIT_FAILURE; - } - - /* Validate required arguments */ - if (!node_name) - { - log_error("Missing required argument: --node_name is mandatory."); - print_node_drop_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node_name); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Prepare SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.node_drop(" - "node_name := '%s', " - "ifexists := %s);", - node_name, - ifexists ? "true" : "false"); - - - /* Execute SQL query */ - res = PQexec(conn, sql); - log_debug0("SQL: %s", sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Check for NULL result */ - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - /* Clean up */ - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int -handle_node_add_interface_command(int argc, char *argv[]) -{ - int option_index = 0; - int c; - const char *conninfo = NULL; - char *node_name = NULL; - char *node = NULL; - char *interface_name = NULL; - char *dsn = NULL; - PGconn *conn = NULL; - PGresult *res = NULL; - char sql[1024]; - - static struct option long_options[] = { - {"node", required_argument, 0, 't'}, - {"node_name", required_argument, 0, 'n'}, - {"interface_name", required_argument, 0, 'i'}, - {"dsn", required_argument, 0, 's'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - /* Parse command-line arguments */ - while ((c = getopt_long(argc, argv, "t:n:i:s:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 't': - node = optarg; - break; - case 'n': - node_name = optarg; - break; - - case 'i': - interface_name = optarg; - break; - case 's': - dsn = optarg; - break; - case 'h': - print_node_add_interface_help(); - return EXIT_SUCCESS; - default: - print_node_add_interface_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node_name) - { - log_error("Missing required argument: --node_name is mandatory."); - print_node_add_interface_help(); - return EXIT_FAILURE; - } - - if (!interface_name) - { - log_error("Missing required argument: --interface_name is mandatory."); - print_node_add_interface_help(); - return EXIT_FAILURE; - } - - if (!dsn) - { - log_error("Missing required argument: --dsn is mandatory."); - print_node_add_interface_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node_name); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Prepare SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.node_add_interface(" - "node_name := '%s', " - "interface_name := '%s', " - "dsn := '%s');", - node_name, - interface_name, - dsn); - - log_debug0("SQL: %s", sql); - - /* Execute SQL query */ - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Check for NULL result */ - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Clean up */ - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int -handle_node_drop_interface_command(int argc, char *argv[]) -{ - int option_index = 0; - int c; - const char *conninfo = NULL; - char *node_name = NULL; - char *node = NULL; - char *interface_name = NULL; - PGconn *conn = NULL; - PGresult *res = NULL; - char sql[1024]; - - static struct option long_options[] = { - {"node", required_argument, 0, 't'}, - {"node_name", required_argument, 0, 'n'}, - {"interface_name", required_argument, 0, 'i'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - /* Parse command-line arguments */ - while ((c = getopt_long(argc, argv, "t:n:i:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 't': - node = optarg; - break; - case 'n': - node_name = optarg; - break; - case 'i': - interface_name = optarg; - break; - case 'h': - print_node_drop_interface_help(); - return EXIT_SUCCESS; - default: - print_node_drop_interface_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node_name) - { - log_error("Missing required argument: --node_name is mandatory."); - print_node_drop_interface_help(); - return EXIT_FAILURE; - } - - if (!interface_name) - { - log_error("Missing required argument: --interface_name is mandatory."); - print_node_drop_interface_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node_name); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Prepare SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.node_drop_interface(" - "node_name := '%s', " - "interface_name := '%s');", - node_name, - interface_name); - - log_debug0("SQL: %s", sql); - - /* Execute SQL query */ - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Check for NULL result */ - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Clean up */ - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -static void -print_node_spock_version_help(void) -{ - printf("Usage: spockctrl node spock-version --node=[node_name]\n"); - printf("Show Spock version.\n"); - printf("Options:\n"); - printf(" --node Node name (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_node_pg_version_help(void) -{ - printf("Usage: spockctrl node pg-version --node=[node_name]\n"); - printf("Show PostgreSQL version.\n"); - printf("Options:\n"); - printf(" --node Node name (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_node_status_help(void) -{ - printf("Usage: spockctrl node status --node=[node_name]\n"); - printf("Show node status.\n"); - printf("Options:\n"); - printf(" --node Node name (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_node_gucs_help(void) -{ - printf("Usage: spockctrl node gucs --node=[node_name]\n"); - printf("Show node GUCs.\n"); - printf("Options:\n"); - printf(" --node Node name (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_node_create_help(void) -{ - printf("Usage: spockctrl node create --node=[target_node] --node_name=[node_name] --dsn=[dsn] [options]\n"); - printf("Create a node.\n"); - printf("Options:\n"); - printf(" --node Target node (required)\n"); - printf(" --node_name Node name (required)\n"); - printf(" --dsn Data source name (required)\n"); - printf(" --location Location of the node (optional)\n"); - printf(" --country Country of the node (optional)\n"); - printf(" --info Additional info about the node (optional)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_node_drop_help(void) -{ - printf("Usage: spockctrl node drop --node_name=[node_name] [options]\n"); - printf("Drop a node.\n"); - printf("Options:\n"); - printf(" --node_name Node name (required)\n"); - printf(" --ifexists Drop node only if it exists (optional)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_node_add_interface_help(void) -{ - printf("Usage: spockctrl node add-interface --node_name=[node_name] --interface_name=[interface_name] --dsn=[dsn]\n"); - printf("Add an interface to a node.\n"); - printf("Options:\n"); - printf(" --node_name Node name (required)\n"); - printf(" --interface_name Interface name (required)\n"); - printf(" --dsn Data source name (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_node_drop_interface_help(void) -{ - printf("Usage: spockctrl node drop-interface --node_name=[node_name] --interface_name=[interface_name]\n"); - printf("Drop an interface from a node.\n"); - printf("Options:\n"); - printf(" --node_name Node name (required)\n"); - printf(" --interface_name Interface name (required)\n"); - printf(" --help Show this help message\n"); -} - -int -get_pg_version(const char *conninfo, char *pg_version) -{ - PGconn *conn; - PGresult *res; - char sql[256] = "SELECT version();"; - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to database with conninfo: %s", conninfo); - return EXIT_FAILURE; - } - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("Failed to execute query: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - log_debug0("SQL: %s", sql); - - snprintf(pg_version, 256, "\n%s", PQgetvalue(res, 0, 0)); - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} diff --git a/utils/spockctrl/src/repset.c b/utils/spockctrl/src/repset.c deleted file mode 100644 index 47bfdd67..00000000 --- a/utils/spockctrl/src/repset.c +++ /dev/null @@ -1,1012 +0,0 @@ -/*------------------------------------------------------------------------- - * - * repset.c - * replication set management and command handling functions - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include -#include -#include -#include -#include "dbconn.h" -#include -#include -#include "repset.h" -#include "conf.h" -#include "logger.h" - -static void print_repset_create_help(void); -static void print_repset_alter_help(void); -static void print_repset_drop_help(void); -static void print_repset_add_table_help(void); -static void print_repset_remove_table_help(void); -static void print_repset_add_partition_help(void); -static void print_repset_remove_partition_help(void); -static void print_repset_list_tables_help(void); - -void -print_repset_help(void) -{ - printf("Usage: spockctrl repset [options]\n"); - printf("Subcommands:\n"); - printf(" create Create a new replication set\n"); - printf(" alter Alter an existing replication set\n"); - printf(" drop Drop a replication set\n"); - printf(" add-table Add a table to a replication set\n"); - printf(" remove-table Remove a table from a replication set\n"); - printf(" add-partition Add a partition to a replication set\n"); - printf(" remove-partition Remove a partition from a replication set\n"); - printf(" list-tables List tables in a replication set\n"); -} - -int -handle_repset_command(int argc, char *argv[]) -{ - int i; - struct { - const char *cmd; - int (*func)(int, char *[]); - } commands[] = { - {"create", handle_repset_create_command}, - {"alter", handle_repset_alter_command}, - {"drop", handle_repset_drop_command}, - {"add-table", handle_repset_add_table_command}, - {"remove-table", handle_repset_remove_table_command}, - {"add-partition", handle_repset_add_partition_command}, - {"remove-partition", handle_repset_remove_partition_command}, - {"list-tables", handle_repset_list_tables_command} - }; - - if (argc < 3) - { - log_error("No subcommand provided for repset."); - print_repset_help(); - return EXIT_FAILURE; - } - - if (strcmp(argv[2], "--help") == 0) - { - print_repset_help(); - return EXIT_SUCCESS; - } - - for (i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) - { - if (strcmp(argv[1], commands[i].cmd) == 0) - { - commands[i].func(argc, &argv[0]); - return EXIT_SUCCESS; - } - } - log_error("Unknown subcommand for repset..."); - print_repset_help(); - return EXIT_FAILURE; -} - -int -handle_repset_create_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"set_name", required_argument, 0, 's'}, - {"replicate_insert", required_argument, 0, 'i'}, - {"replicate_update", required_argument, 0, 'u'}, - {"replicate_delete", required_argument, 0, 'e'}, - {"replicate_truncate", required_argument, 0, 't'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *set_name = NULL; - char *replicate_insert = NULL; - char *replicate_update = NULL; - char *replicate_delete = NULL; - char *replicate_truncate = NULL; - const char *conninfo = NULL; - char *conninfo_allocated = NULL; - int option_index = 0; - int c; - PGconn *conn; - PGresult *res; - char sql[2048]; - - optind = 1; - - while ((c = getopt_long(argc, argv, "n:s:i:u:e:t:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - set_name = optarg; - break; - case 'i': - replicate_insert = optarg; - break; - case 'u': - replicate_update = optarg; - break; - case 'e': - replicate_delete = optarg; - break; - case 't': - replicate_truncate = optarg; - break; - case 'h': - print_repset_create_help(); - return EXIT_SUCCESS; - default: - print_repset_create_help(); - return EXIT_FAILURE; - } - } - - if (!node || !set_name || !replicate_insert || !replicate_update || !replicate_delete || !replicate_truncate) - { - log_error("Missing required arguments for repset create."); - print_repset_create_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - free(conninfo_allocated); // Free allocated memory - return EXIT_FAILURE; - } - - snprintf(sql, sizeof(sql), - "SELECT spock.repset_create(" - "set_name := '%s', " - "replicate_insert := '%s', " - "replicate_update := '%s', " - "replicate_delete := '%s', " - "replicate_truncate := '%s');", - set_name, replicate_insert, replicate_update, replicate_delete, replicate_truncate); - - log_debug0("SQL: %s", sql); - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - free(conninfo_allocated); // Free allocated memory - return EXIT_FAILURE; - } - - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - free(conninfo_allocated); // Free allocated memory - return EXIT_FAILURE; - } - - PQclear(res); - PQfinish(conn); - free(conninfo_allocated); // Free allocated memory - return EXIT_SUCCESS; -} - -int -handle_repset_alter_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"set_name", required_argument, 0, 's'}, - {"replicate_insert", required_argument, 0, 'i'}, - {"replicate_update", required_argument, 0, 'u'}, - {"replicate_delete", required_argument, 0, 'e'}, - {"replicate_truncate", required_argument, 0, 't'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *set_name = NULL; - char *replicate_insert = NULL; - char *replicate_update = NULL; - char *replicate_delete = NULL; - char *replicate_truncate = NULL; - const char *conninfo = NULL; - char *conninfo_allocated = NULL; - - int option_index = 0; - int c; - PGconn *conn; - PGresult *res; - char sql[2048]; - - while ((c = getopt_long(argc, argv, "n:s:i:u:e:t:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - set_name = optarg; - break; - case 'i': - replicate_insert = optarg; - break; - case 'u': - replicate_update = optarg; - break; - case 'e': - replicate_delete = optarg; - break; - case 't': - replicate_truncate = optarg; - break; - case 'h': - print_repset_alter_help(); - return EXIT_SUCCESS; - default: - print_repset_alter_help(); - return EXIT_FAILURE; - } - } - - if (!node || !set_name || !replicate_insert || !replicate_update || !replicate_delete || !replicate_truncate) - { - log_error("Missing required arguments for repset alter."); - print_repset_alter_help(); - return EXIT_FAILURE; - } - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - free(conninfo_allocated); // Free allocated memory - return EXIT_FAILURE; - } - - snprintf(sql, sizeof(sql), - "SELECT spock.repset_alter(" - "set_name := '%s', " - "replicate_insert := '%s', " - "replicate_update := '%s', " - "replicate_delete := '%s', " - "replicate_truncate := '%s');", - set_name, replicate_insert, replicate_update, replicate_delete, replicate_truncate); - - log_debug0("SQL: %s", sql); - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - free(conninfo_allocated); - return EXIT_FAILURE; - } - - PQclear(res); - PQfinish(conn); - free(conninfo_allocated); - return EXIT_SUCCESS; -} - -int -handle_repset_drop_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"set_name", required_argument, 0, 's'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *set_name = NULL; - const char *conninfo = NULL; - - int option_index = 0; - int c; - PGconn *conn; - PGresult *res; - char sql[2048]; - - optind = 1; - while ((c = getopt_long(argc, argv, "n:s:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - set_name = optarg; - break; - case 'h': - print_repset_drop_help(); - return EXIT_SUCCESS; - default: - print_repset_drop_help(); - return EXIT_FAILURE; - } - } - - if (!node || !set_name) - { - log_error("Missing required arguments for repset drop."); - print_repset_drop_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - snprintf(sql, sizeof(sql), - "SELECT spock.repset_drop(" - "set_name := '%s');", - set_name); - - log_debug0("SQL: %s", sql); - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int -handle_repset_add_table_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"replication_set", required_argument, 0, 'r'}, - {"table", required_argument, 0, 'a'}, - {"synchronize_data", required_argument, 0, 'y'}, - {"columns", required_argument, 0, 'c'}, - {"row_filter", required_argument, 0, 'f'}, - {"include_partitions", required_argument, 0, 'p'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *replication_set = NULL; - char *table = NULL; - char *synchronize_data = NULL; - char *columns = NULL; - char *row_filter = NULL; - char *include_partitions = NULL; - const char *conninfo = NULL; - - int option_index = 0; - int c; - PGconn *conn; - PGresult *res; - char sql[2048]; - - while ((c = getopt_long(argc, argv, "n:r:a:y:c:f:p:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 'r': - replication_set = optarg; - break; - case 'a': - table = optarg; - break; - case 'y': - synchronize_data = optarg; - break; - case 'c': - columns = optarg; - break; - case 'f': - row_filter = optarg; - break; - case 'p': - include_partitions = optarg; - break; - case 'h': - print_repset_add_table_help(); - return EXIT_SUCCESS; - default: - print_repset_add_table_help(); - return EXIT_FAILURE; - } - } - - if (!node || !replication_set || !table || !synchronize_data || !columns || !row_filter || !include_partitions) - { - log_error("Missing required arguments for repset add-table."); - print_repset_add_table_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - snprintf(sql, sizeof(sql), - "SELECT spock.repset_add_table(" - "replication_set := '%s', " - "table := '%s', " - "synchronize_data := '%s', " - "columns := '%s', " - "row_filter := '%s', " - "include_partitions := '%s');", - replication_set, table, synchronize_data, columns, row_filter, include_partitions); - - log_debug0("SQL: %s", sql); - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int -handle_repset_remove_table_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"replication_set", required_argument, 0, 'r'}, - {"table", required_argument, 0, 'a'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *replication_set = NULL; - char *table = NULL; - const char *conninfo = NULL; - - int option_index = 0; - int c; - PGconn *conn; - PGresult *res; - char sql[2048]; - - while ((c = getopt_long(argc, argv, "n:r:a:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 'r': - replication_set = optarg; - break; - case 'a': - table = optarg; - break; - case 'h': - print_repset_remove_table_help(); - return EXIT_SUCCESS; - default: - print_repset_remove_table_help(); - return EXIT_FAILURE; - } - } - - if (!node || !replication_set || !table) - { - log_error("Missing required arguments for repset remove-table."); - print_repset_remove_table_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - snprintf(sql, sizeof(sql), - "SELECT spock.repset_remove_table(" - "replication_set := '%s', " - "table := '%s');", - replication_set, table); - - log_debug0("SQL: %s", sql); - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int -handle_repset_add_partition_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"parent_table", required_argument, 0, 'b'}, - {"partition", required_argument, 0, 'q'}, - {"row_filter", required_argument, 0, 'f'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *parent_table = NULL; - char *partition = NULL; - char *row_filter = NULL; - const char *conninfo = NULL; - - int option_index = 0; - int c; - PGconn *conn; - PGresult *res; - char sql[2048]; - - while ((c = getopt_long(argc, argv, "n:b:q:f:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 'b': - parent_table = optarg; - break; - case 'q': - partition = optarg; - break; - case 'f': - row_filter = optarg; - break; - case 'h': - print_repset_add_partition_help(); - return EXIT_SUCCESS; - default: - print_repset_add_partition_help(); - return EXIT_FAILURE; - } - } - - if (!node || !parent_table || !partition || !row_filter) - { - log_error("Missing required arguments for repset add-partition."); - print_repset_add_partition_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - snprintf(sql, sizeof(sql), - "SELECT spock.repset_add_partition(" - "parent_table := '%s', " - "partition := '%s', " - "row_filter := '%s');", - parent_table, partition, row_filter); - - log_debug0("SQL: %s", sql); - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int -handle_repset_remove_partition_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"parent_table", required_argument, 0, 'b'}, - {"partition", required_argument, 0, 'q'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *parent_table = NULL; - char *partition = NULL; - const char *conninfo = NULL; - - int option_index = 0; - int c; - PGconn *conn; - PGresult *res; - char sql[2048]; - - while ((c = getopt_long(argc, argv, "n:b:q:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 'b': - parent_table = optarg; - break; - case 'q': - partition = optarg; - break; - case 'h': - print_repset_remove_partition_help(); - return EXIT_SUCCESS; - default: - print_repset_remove_partition_help(); - return EXIT_FAILURE; - } - } - - if (!node || !parent_table || !partition) - { - log_error("Missing required arguments for repset remove-partition."); - print_repset_remove_partition_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - snprintf(sql, sizeof(sql), - "SELECT spock.repset_remove_partition(" - "parent_table := '%s', " - "partition := '%s');", - parent_table, partition); - - log_debug0("SQL: %s", sql); - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int -handle_repset_list_tables_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"schema", required_argument, 0, 'm'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *schema = NULL; - const char *conninfo = NULL; - - int option_index = 0; - int c; - PGconn *conn; - PGresult *res; - char sql[2048]; - char condition[256]; - - while ((c = getopt_long(argc, argv, "n:m:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 'm': - schema = optarg; - break; - case 'h': - print_repset_list_tables_help(); - return EXIT_SUCCESS; - default: - print_repset_list_tables_help(); - return EXIT_FAILURE; - } - } - - if (!node || !schema) - { - log_error("Missing required arguments for repset list-tables."); - print_repset_list_tables_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - snprintf(condition, sizeof(condition), "nspname = '%s' AND relname = '%s'", schema, node); - - snprintf(sql, sizeof(sql), - "SELECT * FROM spock.TABLES WHERE %s;", condition); - - log_debug0("SQL: %s", sql); - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -void -print_repset_create_help(void) -{ - printf("Usage: spockctrl repset create [OPTIONS]\n"); - printf("Create a new replication set\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --set_name Name of the replication set (required)\n"); - printf(" --replicate_insert Replicate insert operations (required)\n"); - printf(" --replicate_update Replicate update operations (required)\n"); - printf(" --replicate_delete Replicate delete operations (required)\n"); - printf(" --replicate_truncate Replicate truncate operations (required)\n"); - printf(" --help Show this help message\n"); -} - -void -print_repset_alter_help(void) -{ - printf("Usage: spockctrl repset alter [OPTIONS]\n"); - printf("Alter an existing replication set\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --set_name Name of the replication set (required)\n"); - printf(" --replicate_insert Replicate insert operations (required)\n"); - printf(" --replicate_update Replicate update operations (required)\n"); - printf(" --replicate_delete Replicate delete operations (required)\n"); - printf(" --replicate_truncate Replicate truncate operations (required)\n"); - printf(" --help Show this help message\n"); -} - -void -print_repset_drop_help(void) -{ - printf("Usage: spockctrl repset drop [OPTIONS]\n"); - printf("Drop a replication set\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --set_name Name of the replication set (required)\n"); - printf(" --help Show this help message\n"); -} - -void -print_repset_add_table_help(void) -{ - printf("Usage: spockctrl repset add-table [OPTIONS]\n"); - printf("Add a table to a replication set\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --replication_set Name of the replication set (required)\n"); - printf(" --table Name of the table (required)\n"); - printf(" --synchronize_data Synchronize data (required)\n"); - printf(" --columns Columns to replicate (required)\n"); - printf(" --row_filter Row filter (required)\n"); - printf(" --include_partitions Include partitions (required)\n"); - printf(" --help Show this help message\n"); -} - -void -print_repset_remove_table_help(void) -{ - printf("Usage: spockctrl repset remove-table [OPTIONS]\n"); - printf("Remove a table from a replication set\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --replication_set Name of the replication set (required)\n"); - printf(" --table Name of the table (required)\n"); - printf(" --help Show this help message\n"); -} - -void -print_repset_add_partition_help(void) -{ - printf("Usage: spockctrl repset add-partition [OPTIONS]\n"); - printf("Add a partition to a replication set\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --parent_table Name of the parent table (required)\n"); - printf(" --partition Name of the partition (required)\n"); - printf(" --row_filter Row filter (required)\n"); - printf(" --help Show this help message\n"); -} - -void -print_repset_remove_partition_help(void) -{ - printf("Usage: spockctrl repset remove-partition [OPTIONS]\n"); - printf("Remove a partition from a replication set\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --parent_table Name of the parent table (required)\n"); - printf(" --partition Name of the partition (required)\n"); - printf(" --help Show this help message\n"); -} - -void -print_repset_list_tables_help(void) -{ - printf("Usage: spockctrl repset list-tables [OPTIONS]\n"); - printf("List tables in a replication set\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --schema Schema name (required)\n"); - printf(" --help Show this help message\n"); -} \ No newline at end of file diff --git a/utils/spockctrl/src/slot.c b/utils/spockctrl/src/slot.c deleted file mode 100644 index 5414a539..00000000 --- a/utils/spockctrl/src/slot.c +++ /dev/null @@ -1,398 +0,0 @@ -/*------------------------------------------------------------------------- - * - * slot.c - * replication slot management and command handling functions - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include -#include -#include -#include -#include "dbconn.h" -#include "node.h" -#include "logger.h" -#include "util.h" -#include "conf.h" -#include "slot.h" - -void print_slot_create_help(void); -void print_slot_drop_help(void); -void print_slot_enable_help(void); -void print_slot_disable_help(void); - -void print_slot_help(void) -{ - printf("Usage: spockctrl slot [options]\n"); - printf("Subcommands:\n"); - printf(" create Create a replication slot\n"); - printf(" drop Drop a replication slot\n"); - printf(" enable Enable a replication slot\n"); - printf(" disable Disable a replication slot\n"); - printf(" --help Show this help message\n"); -} - -int handle_slot_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - struct { - const char *cmd; - int (*func)(int, char *[]); - } commands[] = { - {"create", handle_slot_create_command}, - {"drop", handle_slot_drop_command}, - {"enable", handle_slot_enable_command}, - {"disable", handle_slot_disable_command}, - }; - - int option_index = 0; - int c; - - if (argc < 2) { - log_error("No subcommand provided for slot."); - print_slot_help(); - return EXIT_FAILURE; - } - - for (size_t i = 0; i < sizeof(commands)/sizeof(commands[0]); i++) { - if (strcmp(argv[1], commands[i].cmd) == 0) - return commands[i].func(argc, argv); - } - - while ((c = getopt_long(argc, argv, "h", long_options, &option_index)) != -1) { - switch (c) { - case 'h': - print_slot_help(); - return EXIT_SUCCESS; - default: - print_slot_help(); - return EXIT_FAILURE; - } - } - - log_error("Unknown subcommand for slot."); - print_slot_help(); - return EXIT_FAILURE; -} - -int handle_slot_create_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"slot", required_argument, 0, 's'}, - {"plugin", required_argument, 0, 'p'}, - {"temporary", no_argument, 0, 't'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *slot_name = NULL; - char *node_name = NULL; - char *plugin_name = "spock_output"; - int temporary = 0; - int option_index= 0; - int c = 0; - const char *conninfo= NULL; - PGconn *conn = NULL; - char query[512]; - PGresult *res = NULL; - - optind = 1; - while ((c = getopt_long(argc, argv, "n:s:p:th", long_options, &option_index)) != -1) { - switch (c) { - case 'n': - node_name = optarg; - break; - case 's': - slot_name = optarg; - break; - case 'p': - plugin_name = optarg; - break; - case 't': - temporary = 1; - break; - case 'h': - print_slot_create_help(); - return EXIT_SUCCESS; - default: - print_slot_create_help(); - return EXIT_FAILURE; - } - } - - if (!slot_name) { - log_error("Slot name is required for create."); - print_slot_create_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node_name); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node_name ? node_name : "(default)"); - print_slot_create_help(); - return EXIT_FAILURE; - } - - conn = PQconnectdb(conninfo); - if (!conn || PQstatus(conn) != CONNECTION_OK) - { - log_error("Connection to database failed: %s", conn ? PQerrorMessage(conn) : "NULL connection"); - if (conn) PQfinish(conn); - return EXIT_FAILURE; - } - /* Build the SQL query */ - snprintf(query, sizeof(query), - "SELECT pg_create_logical_replication_slot(" - "slot_name := '%s', " - "plugin := '%s', " - "temporary := %s);", - slot_name, - plugin_name, - temporary ? "true" : "false"); - - res = PQexec(conn, query); - if (!res || PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("Failed to create replication slot: %s", conn ? PQerrorMessage(conn) : "NULL connection"); - log_error("SQL: %s", query); - if (res) PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int handle_slot_drop_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"slot", required_argument, 0, 's'}, - {"node", required_argument, 0, 'n'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *slot_name = NULL; - char *node_name = NULL; - int option_index = 0; - int c = 0; - const char *conninfo = NULL; - PGconn *conn = NULL; - char query[256]; - PGresult *res = NULL; - - optind = 2; - while ((c = getopt_long(argc, argv, "s:n:h", long_options, &option_index)) != -1) { - switch (c) { - case 's': - slot_name = optarg; - break; - case 'n': - node_name = optarg; - break; - case 'h': - print_slot_drop_help(); - return EXIT_SUCCESS; - default: - print_slot_drop_help(); - return EXIT_FAILURE; - } - } - - if (!slot_name) { - log_error("Slot name is required for drop."); - print_slot_drop_help(); - return EXIT_FAILURE; - } - - if (!node_name) { - log_error("Node name is required for drop."); - print_slot_drop_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node_name); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node_name); - print_slot_drop_help(); - return EXIT_FAILURE; - } - - conn = PQconnectdb(conninfo); - if (!conn || PQstatus(conn) != CONNECTION_OK) - { - log_error("Connection to database failed: %s", conn ? PQerrorMessage(conn) : "NULL connection"); - if (conn) PQfinish(conn); - return EXIT_FAILURE; - } - - snprintf(query, sizeof(query), - "SELECT pg_drop_replication_slot('%s');", - slot_name); - - res = PQexec(conn, query); - if (!res || PQresultStatus(res) != PGRES_COMMAND_OK) - { - log_error("Failed to drop replication slot: %s", conn ? PQerrorMessage(conn) : "NULL connection"); - if (res) PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - printf("Replication slot dropped: %s\n", slot_name); - - PQclear(res); - PQfinish(conn); - - return EXIT_SUCCESS; -} - -int handle_slot_enable_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"slot", required_argument, 0, 's'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *slot_name = NULL; - char *node_name = NULL; - int option_index = 0; - int c = 0; - - optind = 2; - while ((c = getopt_long(argc, argv, "n:s:h", long_options, &option_index)) != -1) { - switch (c) { - case 'n': - node_name = optarg; - break; - case 's': - slot_name = optarg; - break; - case 'h': - print_slot_enable_help(); - return EXIT_SUCCESS; - default: - print_slot_enable_help(); - return EXIT_FAILURE; - } - } - - if (!node_name) { - log_error("Node name is required for enable."); - print_slot_enable_help(); - return EXIT_FAILURE; - } - - if (!slot_name) { - log_error("Slot name is required for enable."); - print_slot_enable_help(); - return EXIT_FAILURE; - } - - printf("Enabling replication slot: %s on node: %s (no-op, not implemented)\n", slot_name, node_name); - return EXIT_SUCCESS; -} - -int handle_slot_disable_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"slot", required_argument, 0, 's'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *slot_name = NULL; - char *node_name = NULL; - int option_index = 0; - int c = 0; - - optind = 2; - while ((c = getopt_long(argc, argv, "n:s:h", long_options, &option_index)) != -1) { - switch (c) { - case 'n': - node_name = optarg; - break; - case 's': - slot_name = optarg; - break; - case 'h': - print_slot_disable_help(); - return EXIT_SUCCESS; - default: - print_slot_disable_help(); - return EXIT_FAILURE; - } - } - - if (!node_name) { - log_error("Node name is required for disable."); - print_slot_disable_help(); - return EXIT_FAILURE; - } - - if (!slot_name) { - log_error("Slot name is required for disable."); - print_slot_disable_help(); - return EXIT_FAILURE; - } - - printf("Disabling replication slot: %s on node: %s (no-op, not implemented)\n", slot_name, node_name); - return EXIT_SUCCESS; -} - -void print_slot_create_help(void) -{ - printf("Usage: spockctrl slot create --node --slot [--plugin ] [--temporary]\n"); - printf("Create a replication slot\n"); - printf("Options:\n"); - printf(" --node Name of the node (optional)\n"); - printf(" --slot Name of the replication slot (required)\n"); - printf(" --plugin Output plugin to use (default: test_decoding)\n"); - printf(" --temporary Create a temporary slot (optional)\n"); - printf(" --help Show this help message\n"); -} - -void print_slot_drop_help(void) -{ - printf("Usage: spockctrl slot drop --node --slot \n"); - printf("Drop a replication slot\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --slot Name of the replication slot (required)\n"); - printf(" --help Show this help message\n"); -} -void print_slot_enable_help(void) -{ - printf("Usage: spockctrl slot enable --node --slot \n"); - printf("Enable a replication slot (no-op)\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --slot Name of the replication slot (required)\n"); - printf(" --help Show this help message\n"); -} - -void print_slot_disable_help(void) -{ - printf("Usage: spockctrl slot disable --node --slot \n"); - printf("Disable a replication slot (no-op)\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --slot Name of the replication slot (required)\n"); - printf(" --help Show this help message\n"); -} \ No newline at end of file diff --git a/utils/spockctrl/src/spock.c b/utils/spockctrl/src/spock.c deleted file mode 100644 index 63b02017..00000000 --- a/utils/spockctrl/src/spock.c +++ /dev/null @@ -1,279 +0,0 @@ -/*------------------------------------------------------------------------- - * - * spock.c - * spock extension command handling functions - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include -#include -#include -#include -#include -#include "dbconn.h" -#include "conf.h" -#include "logger.h" - -extern Config config; - -static void -print_spock_wait_for_sync_event_help(void) -{ - printf("Usage: spockctrl spock wait-for-sync-event [OPTIONS]\n"); - printf("Wait for a sync event to be confirmed.\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --origin-name Origin name (mutually exclusive with --origin-oid)\n"); - printf(" --origin-oid Origin OID (mutually exclusive with --origin-name)\n"); - printf(" --lsn LSN to wait for (required)\n"); - printf(" --timeout Timeout in seconds (optional, default 0)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_spock_sync_event_help(void) -{ - printf("Usage: spockctrl spock sync-event [OPTIONS]\n"); - printf("Trigger a sync event.\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --help Show this help message\n"); -} - -int -handle_spock_wait_for_sync_event_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"origin-name", required_argument, 0, 1}, - {"origin-oid", required_argument, 0, 2}, - {"lsn", required_argument, 0, 'l'}, - {"timeout", required_argument, 0, 't'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *origin_name = NULL; - char *origin_oid = NULL; - char *lsn = NULL; - int timeout = 0; - int option_index = 0; - int c; - const char *conninfo = NULL; - PGconn *conn = NULL; - char sql[512]; - PGresult *res = NULL; - int result = -1; - const char *origin_type = NULL; - const char *origin = NULL; - - optind = 1; - while ((c = getopt_long(argc, argv, "n:l:t:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 1: - origin_name = optarg; - break; - case 2: - origin_oid = optarg; - break; - case 'l': - lsn = optarg; - break; - case 't': - timeout = atoi(optarg); - break; - case 'h': - print_spock_wait_for_sync_event_help(); - return EXIT_SUCCESS; - default: - print_spock_wait_for_sync_event_help(); - return EXIT_FAILURE; - } - } - - if (!node || !lsn || (!origin_name && !origin_oid) || (origin_name && origin_oid)) - { - log_error("Missing required arguments: --node, --lsn, and either --origin-name or --origin-oid (but not both) are mandatory."); - print_spock_wait_for_sync_event_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - origin_type = origin_name ? "name" : "oid"; - origin = origin_name ? origin_name : origin_oid; - - if (!conn || !origin_type || !origin || !lsn) - { - PQfinish(conn); - log_error("Invalid arguments for wait_for_sync_event."); - return EXIT_FAILURE; - } - - if (strcmp(origin_type, "name") == 0) - { - snprintf(sql, sizeof(sql), - "SELECT spock.wait_for_sync_event('%s', '%s'::pg_lsn, %d);", - origin, - lsn, - timeout - ); - } - else if (strcmp(origin_type, "oid") == 0) - { - snprintf(sql, sizeof(sql), - "SELECT spock.wait_for_sync_event(%s::oid, '%s'::pg_lsn, %d);", - origin, - lsn, - timeout - ); - } - else - { - PQfinish(conn); - log_error("Invalid origin_type: %s", origin_type); - return EXIT_FAILURE; - } - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL error: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - if (PQntuples(res) == 1) - { - const char *val = PQgetvalue(res, 0, 0); - - result = (strcmp(val, "t") == 0 || strcmp(val, "true") == 0) ? 1 : 0; - } - PQclear(res); - PQfinish(conn); - - if (result == -1) - { - log_error("wait_for_sync_event failed."); - return EXIT_FAILURE; - } - log_debug0("wait_for_sync_event returned: %s", result ? "true" : "false"); - return result ? EXIT_SUCCESS : EXIT_FAILURE; -} - -int -handle_spock_sync_event_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - int option_index = 0; - int c; - const char *conninfo = NULL; - PGconn *conn = NULL; - char sql[512]; - PGresult *res = NULL; - int result = -1; - - optind = 1; - while ((c = getopt_long(argc, argv, "n:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 'h': - print_spock_sync_event_help(); - return EXIT_SUCCESS; - default: - print_spock_sync_event_help(); - return EXIT_FAILURE; - } - } - - if (!node) - { - log_error("Missing required arguments: --node is mandatory."); - print_spock_sync_event_help(); - return EXIT_FAILURE; - } - - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - return EXIT_FAILURE; - } - - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - if (!conn) - { - PQfinish(conn); - log_error("Invalid arguments for sync_event."); - return EXIT_FAILURE; - } - - snprintf(sql, sizeof(sql), - "SELECT spock.sync_event();"); - - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL error: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - if (PQntuples(res) == 1) - { - const char *val = PQgetvalue(res, 0, 0); - - result = (strcmp(val, "t") == 0 || strcmp(val, "true") == 0) ? 1 : 0; - } - PQclear(res); - PQfinish(conn); - - if (result == -1) - { - log_error("sync_event failed."); - return EXIT_FAILURE; - } - log_debug0("sync_event returned: %s", result ? "true" : "false"); - return EXIT_SUCCESS; -} diff --git a/utils/spockctrl/src/spockctrl.c b/utils/spockctrl/src/spockctrl.c deleted file mode 100644 index 6e3f879c..00000000 --- a/utils/spockctrl/src/spockctrl.c +++ /dev/null @@ -1,217 +0,0 @@ -/*------------------------------------------------------------------------- - * - * spockctrl.c - * main entry point for spockctrl command-line tool - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include -#include -#include -#include -#include -#include "logger.h" -#include "node.h" -#include "repset.h" -#include "conf.h" -#include "sub.h" -#include "sql.h" -#include "workflow.h" - -#define VERSION "1.0.0" -#define CONFIG_FILE "spockctrl.json" -#define DEFAULT_FORMAT "table" - -static void print_help(void); -static char *format = DEFAULT_FORMAT; -static int verbose = 0; - -static void -print_help(void) -{ - printf("Usage: spockctrl [command] [subcommand] [subcommand options] [main options]\n"); - printf("Commands:\n"); - printf(" repset Replication set management commands\n"); - printf(" sub Subscription management commands\n"); - printf(" node Node management commands\n"); - printf(" sql Execute SQL commands\n"); - printf(" help Display this help message\n"); - printf(" version Display the version information\n"); - printf("\n"); - printf("Main Options:\n"); - printf(" -c, --config Specify the configuration file (required)\n"); - printf(" -w, --workflow Specify the workflow to run (json file)\n"); - printf(" -f, --format Specify output format (json or table)\n"); - printf(" -v, --verbose Enable verbose mode (levels: 0, 1, 2, 3)\n"); - printf(" -h, --help Show this help message\n"); -} - -int -main(int argc, char *argv[]) -{ - char *config_file = CONFIG_FILE; - char *workflow_file = NULL; - const char *command; - Workflow *workflow = NULL; - - struct - { - const char *name; - int (*handler) (int, char **); - } commands[] = { - {"repset", handle_repset_command}, - {"sub", handle_sub_command}, - {"node", handle_node_command}, - {"sql", handle_sql_exec_command}, - {NULL, NULL} - }; - - if (argc < 2) - { - print_help(); - return 0; - } - command = argv[1]; - - if (strcmp(command, "--version") == 0) - { - printf("spockctrl version %s\n", VERSION); - return EXIT_SUCCESS; - } - if (strcmp(command, "--help") == 0 || strcmp(command, "-h") == 0) - { - print_help(); - return EXIT_SUCCESS; - } - - for (int i = 1; i < argc; i++) - { - if (strncmp(argv[i], "--config=", 9) == 0) - { - config_file = argv[i] + 9; - } - else if (strcmp(argv[i], "-c") == 0) - { - if (i + 1 < argc) - { - config_file = argv[++i]; - } - else - { - log_error("Option -c requires an argument."); - print_help(); - return EXIT_FAILURE; - } - } - else if (strcmp(argv[i], "-w") == 0) - { - if (i + 1 < argc) - { - workflow_file = argv[++i]; - } - } - else if (strncmp(argv[i], "--workflow=", 11) == 0) - { - workflow_file = argv[i] + 11; - } - else if (strcmp(argv[i], "-f") == 0) - { - if (i + 1 < argc) - { - format = argv[++i]; - } - else - { - log_error("Option -f requires an argument."); - print_help(); - return EXIT_FAILURE; - } - } - else if (strncmp(argv[i], "--format=", 9) == 0) - { - format = argv[i] + 9; - } - else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) - { - if (i + 1 < argc) - { - verbose = atoi(argv[++i]); - if (verbose < 0 || verbose > 3) - { - log_error("Invalid verbose level specified. Supported levels are: 0, 1, 2, 3"); - print_help(); - return EXIT_FAILURE; - } - } - else - { - log_error("Option -v/--verbose requires an argument."); - print_help(); - return EXIT_FAILURE; - } - } - } - if (verbose >= 1) - { - current_log_level = LOG_LEVEL_WARNING; - log_warning("Verbose mode enabled. Level: %d", verbose); - } - if (verbose >= 2) - { - current_log_level = LOG_LEVEL_INFO; - log_info("Log messages will be displayed."); - } - if (verbose == 3) - { - current_log_level = LOG_LEVEL_DEBUG0; - log_debug0("Debug mode enabled."); - } - if (verbose == 4) - { - current_log_level = LOG_LEVEL_DEBUG1; - log_debug1("Debug mode enabled."); - } - - - if (strcmp(format, "json") != 0 && strcmp(format, "table") != 0) - { - log_error("Invalid format specified. Supported formats are: json or table"); - print_help(); - return EXIT_FAILURE; - } - - if (load_config(config_file) != 0) - { - log_error("Failed to load configuration file: %s", config_file); - return EXIT_FAILURE; - } - - if (workflow_file != NULL) - { - log_info("Loading workflow file: %s", workflow_file); - workflow = load_workflow(workflow_file); - if (workflow == NULL) - { - log_error("Failed to load workflow file: %s", workflow_file); - return EXIT_FAILURE; - } - run_workflow(workflow); - return EXIT_SUCCESS; - } - - for (int i = 0; commands[i].name != NULL; i++) - { - if (strcasecmp(command, commands[i].name) == 0) - { - return commands[i].handler(argc - 1, &argv[1]); - } - } - - print_help(); - return EXIT_FAILURE; -} diff --git a/utils/spockctrl/src/sql.c b/utils/spockctrl/src/sql.c deleted file mode 100644 index cdaf69c6..00000000 --- a/utils/spockctrl/src/sql.c +++ /dev/null @@ -1,181 +0,0 @@ -/*------------------------------------------------------------------------- - * - * sql.c - * spockctrl SQL execution utilities - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * - *------------------------------------------------------------------------- - */ -#include -#include -#include -#include -#include -#include -#include -#include -#include "dbconn.h" -#include "conf.h" -#include "sub.h" -#include "logger.h" -#include "sql.h" -#include "util.h" - - -void -print_sql_help(void) -{ - printf("Usage: spockctrl sql [options]\n"); - printf("Options:\n"); - printf(" --sql SQL statement to execute (required, must be in single or double quotes)\n"); - printf(" --node Name of the node (optional)\n"); - printf(" --help Show this help message\n"); - printf("\nNote: --sql must be enclosed in single or double quotes, e.g. --sql='SELECT 1' or --sql=\"SELECT 1\"\n"); -} - -int -handle_sql_exec_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"sql", required_argument, 0, 's'}, - {"help", no_argument, 0, 'h'}, - {"ignore-errors", no_argument, 0, 'i'}, - {0, 0, 0, 0} - }; - - char *node_name = NULL; - char *sql_stmt = NULL; - int option_index = 0; - int c = 0; - const char *conninfo = NULL; - PGconn *conn = NULL; - PGresult *res = NULL; - char out_filename[256]; - FILE *outf = NULL; - int nrows; - int ncols; - int row; - int col; - bool ignore_errors = false; - char *sub_sql; - - optind = 1; - while ((c = getopt_long(argc, argv, "n:s:hi", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node_name = optarg; - break; - case 's': - sql_stmt = optarg; - break; - case 'h': - print_sql_help(); - return EXIT_SUCCESS; - case 'i': - ignore_errors = true; - break; - default: - print_sql_help(); - return EXIT_FAILURE; - } - } - if (!sql_stmt) - { - log_error("SQL statement is required."); - print_sql_help(); - return EXIT_FAILURE; - } - - sub_sql = substitute_sql_vars(sql_stmt); - if (sub_sql == NULL || strlen(sub_sql) == 0) - sub_sql = (char *) sql_stmt; - - conninfo = get_postgres_coninfo(node_name); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node_name ? node_name : "(default)"); - print_sql_help(); - return ignore_errors ? EXIT_SUCCESS : EXIT_FAILURE; - /* Handle ignore - errors */ - } - - conn = PQconnectdb(conninfo); - if (!conn || PQstatus(conn) != CONNECTION_OK) - { - log_error("Connection to database failed: %s", conn ? PQerrorMessage(conn) : "NULL connection"); - if (conn) - PQfinish(conn); - return ignore_errors ? EXIT_SUCCESS : EXIT_FAILURE; - /* Handle ignore - errors */ - } - - res = PQexec(conn, sub_sql); - if (res == NULL || (PQresultStatus(res) != PGRES_COMMAND_OK && PQresultStatus(res) != PGRES_TUPLES_OK)) - { - if (!ignore_errors) - log_error("Failed to execute SQL: %s", conn != NULL ? PQerrorMessage(conn) : "NULL connection"); - if (res != NULL) - PQclear(res); - PQfinish(conn); - return ignore_errors ? EXIT_SUCCESS : EXIT_FAILURE; - /* Handle ignore - errors */ - } - - /* Prepare output file name */ - snprintf(out_filename, sizeof(out_filename), "%s.out", node_name ? node_name : "default"); - outf = fopen(out_filename, "w"); - if (!outf) - { - log_error("Could not open output file '%s' for writing.", out_filename); - PQclear(res); - PQfinish(conn); - return ignore_errors ? EXIT_SUCCESS : EXIT_FAILURE; - /* Handle ignore - errors */ - } - - if (PQresultStatus(res) == PGRES_TUPLES_OK) - { - nrows = PQntuples(res); - ncols = PQnfields(res); - - /* Print column headers */ - for (col = 0; col < ncols; col++) - { - log_debug0("%s%s", PQfname(res, col), (col < ncols - 1) ? "\t" : "\n"); - } - - /* Print rows and write to file as key=value pairs */ - for (row = 0; row < nrows; row++) - { - for (col = 0; col < ncols; col++) - { - log_debug0("%s=%s%s", PQfname(res, col), PQgetvalue(res, row, col), (col < ncols - 1) ? "\t" : "\n"); - fprintf(outf, "%s=%s%s", PQfname(res, col), PQgetvalue(res, row, col), (col < ncols - 1) ? "\t" : "\n"); - } - } - } - else if (PQresultStatus(res) == PGRES_COMMAND_OK) - { - log_debug0("result=%s\n", PQcmdStatus(res)); - } - else - { - log_debug0("result=success\n"); - } - - fclose(outf); - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -void -print_sql_exec_help(void) -{ - /* No longer used, but keep for compatibility if referenced elsewhere */ - print_sql_help(); -} diff --git a/utils/spockctrl/src/sub.c b/utils/spockctrl/src/sub.c deleted file mode 100644 index 452d09f2..00000000 --- a/utils/spockctrl/src/sub.c +++ /dev/null @@ -1,1317 +0,0 @@ -/*------------------------------------------------------------------------- - * - * sub.c - * subscription management and command handling functions - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include -#include -#include -#include -#include -#include -#include "dbconn.h" -#include "conf.h" -#include "sub.h" -#include "logger.h" - -static void print_sub_create_help(void); -static void print_sub_drop_help(void); -static void print_sub_enable_help(void); -static void print_sub_disable_help(void); -static void print_sub_show_status_help(void); -static void print_sub_show_table_help(void); -static void print_sub_resync_table_help(void); -static void print_sub_add_repset_help(void); -static void print_sub_remove_repset_help(void); - -extern Config config; - -void -print_sub_help(void) -{ - printf("Subscription management commands:\n"); - printf(" sub create Create a new subscription\n"); - printf(" sub drop Drop a subscription\n"); - printf(" sub enable Enable a subscription\n"); - printf(" sub disable Disable a subscription\n"); - printf(" sub show-status Show the status of a subscription\n"); - printf(" sub show-table Show the table of a subscription\n"); - printf(" sub resync-table Resync a table in a subscription\n"); - printf(" sub add-repset Add a replication set to a subscription\n"); - printf(" sub remove-repset Remove a replication set from a subscription\n"); -} - -int -handle_sub_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - int option_index = 0; - int c; - int i; - - struct - { - const char *cmd; - int min_args; - int (*func) (int, char *[]); - } commands[] = { - {"create", 10, handle_sub_create_command}, - {"drop", 4, handle_sub_drop_command}, - {"enable", 5, handle_sub_enable_command}, - {"disable", 5, handle_sub_disable_command}, - {"show-status", 4, handle_sub_show_status_command}, - {"show-table", 5, handle_sub_show_table_command}, - {"resync-table", 6, handle_sub_resync_table_command}, - {"add-repset", 5, handle_sub_add_repset_command}, - {"remove-repset", 5, handle_sub_remove_repset_command}, - }; - - if (argc < 2) - { - log_error("No subcommand provided for sub."); - print_sub_help(); - return EXIT_FAILURE; - } - - for (i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) - { - if (strcmp(argv[1], commands[i].cmd) == 0) - return commands[i].func(argc, argv); - } - - while ((c = getopt_long(argc, argv, "h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'h': - print_sub_help(); - return EXIT_SUCCESS; - default: - print_sub_help(); - return EXIT_FAILURE; - } - } - - log_error("Unknown subcommand for sub."); - print_sub_help(); - return EXIT_FAILURE; -} - -int -handle_sub_create_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"sub_name", required_argument, 0, 's'}, - {"provider_dsn", required_argument, 0, 'p'}, - {"replication_sets", required_argument, 0, 'r'}, - {"synchronize_structure", required_argument, 0, 'y'}, - {"synchronize_data", required_argument, 0, 'z'}, - {"forward_origins", required_argument, 0, 'f'}, - {"apply_delay", required_argument, 0, 'a'}, - {"force_text_transfer", required_argument, 0, 'x'}, - {"enabled", required_argument, 0, 'e'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *subscription_name = NULL; - char *provider_dsn = NULL; - char *replication_sets = NULL; - char *synchronize_structure = NULL; - char *synchronize_data = NULL; - char *forward_origins = NULL; - char *apply_delay = NULL; - char *force_text_transfer = NULL; - char *enabled = NULL; - const char *conninfo; - const char *db; - PGconn *conn; - PGresult *res; - - char sql[4096]; - char out_filename[256]; - FILE *outf; - int option_index = 0; - int c; - - int nrows; - int ncols; - int row; - int col; - - - optind = 1; - while ((c = getopt_long(argc, argv, "n:s:p:r:y:z:f:a:x:e:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - subscription_name = optarg; - break; - case 'p': - provider_dsn = optarg; - break; - case 'r': - replication_sets = optarg; - break; - case 'y': - synchronize_structure = optarg; - break; - case 'z': - synchronize_data = optarg; - break; - case 'f': - forward_origins = optarg; - break; - case 'a': - apply_delay = optarg; - break; - case 'x': - force_text_transfer = optarg; - break; - case 'e': - enabled = optarg; - break; - case 'h': - print_sub_create_help(); - return EXIT_SUCCESS; - default: - print_sub_create_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node || strlen(node) == 0) - { - log_error("Missing required argument: --node is mandatory."); - print_sub_create_help(); - return EXIT_FAILURE; - } - if (!subscription_name) - { - log_error("Missing required argument: --subscription_name is mandatory."); - print_sub_create_help(); - return EXIT_FAILURE; - } - if (!provider_dsn) - { - log_error("Missing required argument: --provider_dsn is mandatory."); - print_sub_create_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - print_sub_create_help(); - return EXIT_FAILURE; - } - - db = get_postgres_db(node); - if (!db) - { - log_error("Failed to get database name for node '%s'.", node); - print_sub_create_help(); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Build the SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.sub_create(" - "subscription_name := '%s', " - "provider_dsn := '%s', " - "replication_sets := %s, " - "synchronize_structure := %s, " - "synchronize_data := %s, " - "forward_origins := %s, " - "apply_delay := %s, " - "force_text_transfer := %s, " - "enabled := %s" - ");", - subscription_name, - provider_dsn, - replication_sets ? replication_sets : "ARRAY['default','default_insert_only','ddl_sql']", - synchronize_structure ? synchronize_structure : "false", - synchronize_data ? synchronize_data : "false", - forward_origins ? forward_origins : "'{}'::text[]", - apply_delay ? apply_delay : "'0'::interval", - force_text_transfer ? force_text_transfer : "false", - enabled ? enabled : "true" - ); - - /* Execute SQL query */ - res = PQexec(conn, sql); - log_debug0("SQL: %s", sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Prepare output file name */ - snprintf(out_filename, sizeof(out_filename), "%s.out", node); - outf = fopen(out_filename, "w"); - if (!outf) - { - log_error("Could not open output file '%s' for writing.", out_filename); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - if (PQresultStatus(res) == PGRES_TUPLES_OK) - { - nrows = PQntuples(res); - ncols = PQnfields(res); - - /* Print column headers */ - for (col = 0; col < ncols; col++) - { - log_debug0("%s%s", PQfname(res, col), (col < ncols - 1) ? "\t" : "\n"); - } - - /* Print rows and write to file as key=value pairs */ - for (row = 0; row < nrows; row++) - { - for (col = 0; col < ncols; col++) - { - log_debug0("%s=%s%s", PQfname(res, col), PQgetvalue(res, row, col), (col < ncols - 1) ? "\t" : "\n"); - fprintf(outf, "%s=%s%s", PQfname(res, col), PQgetvalue(res, row, col), (col < ncols - 1) ? "\t" : "\n"); - } - } - } - - /* Clean up */ - fclose(outf); - PQclear(res); - PQfinish(conn); - - return EXIT_SUCCESS; -} - -int -handle_sub_drop_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 't'}, - {"sub_name", required_argument, 0, 's'}, - {"ifexists", no_argument, 0, 'e'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *sub_name = NULL; - int ifexists = 0; - const char *conninfo; - const char *db; - PGconn *conn; - PGresult *res; - - char sql[1024]; - int option_index = 0; - int c; - - optind = 1; - - /* Parse command-line options */ - optind = 1; - /* Reset optind to ensure proper parsing */ - - while ((c = getopt_long(argc, argv, "t:s:eh", long_options, &option_index)) != -1) - { - switch (c) - { - case 't': - node = optarg; - break; - case 's': - sub_name = optarg; - break; - case 'e': - ifexists = 1; - break; - case 'h': - print_sub_drop_help(); - return EXIT_SUCCESS; - default: - print_sub_drop_help(); - return EXIT_FAILURE; - } - } - - /* Validate required argument: --node */ - if (!node) - { - log_error("Missing required argument: --node is mandatory."); - print_sub_drop_help(); - return EXIT_FAILURE; - } - - /* Validate required argument: --sub_name */ - if (!sub_name) - { - log_error("Missing required argument: --sub_name is mandatory."); - print_sub_drop_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - print_sub_drop_help(); - return EXIT_FAILURE; - } - - db = get_postgres_db(node); - if (!db) - { - log_error("Failed to get database name for node '%s'.", node); - print_sub_drop_help(); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Build the SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.sub_drop(" - "subscription_name := '%s', " - "ifexists := %s);", - sub_name, - ifexists ? "true" : "false"); - - /* Execute SQL query */ - res = PQexec(conn, sql); - log_debug0("SQL: %s", sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Check for NULL result */ - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Clean up */ - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int -handle_sub_enable_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"sub_name", required_argument, 0, 's'}, - {"immediate", required_argument, 0, 'i'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *sub_name = NULL; - char *immediate = NULL; - const char *conninfo; - const char *db; - PGconn *conn; - PGresult *res; - - char sql[1024]; - int option_index = 0; - int c; - - /* Parse command-line options */ - optind = 1; - /* Reset optind to ensure proper parsing */ - - while ((c = getopt_long(argc, argv, "n:s:i:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - sub_name = optarg; - break; - case 'i': - immediate = optarg; - break; - case 'h': - print_sub_enable_help(); - return EXIT_SUCCESS; - default: - print_sub_enable_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node || !sub_name || !immediate) - { - log_error("Missing required arguments: --node, --sub_name, and --immediate are mandatory."); - print_sub_enable_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - print_sub_enable_help(); - return EXIT_FAILURE; - } - - db = get_postgres_db(node); - if (!db) - { - log_error("Failed to get database name for node '%s'.", node); - print_sub_enable_help(); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Build the SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.sub_enable(" - "subscription_name := '%s', " - "immediate := %s);", - sub_name, - immediate); - - /* Execute SQL query */ - res = PQexec(conn, sql); - log_debug0("SQL: %s", sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Check for NULL result */ - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Clean up */ - PQclear(res); - PQfinish(conn); - - return EXIT_SUCCESS; -} - -int -handle_sub_disable_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"sub_name", required_argument, 0, 's'}, - {"immediate", required_argument, 0, 'i'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *sub_name = NULL; - char *immediate = NULL; - const char *conninfo; - const char *db; - PGconn *conn; - PGresult *res; - - char sql[2048]; - int option_index = 0; - int c; - - optind = 1; - /* Reset optind to ensure proper parsing */ - while ((c = getopt_long(argc, argv, "n:s:i:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - sub_name = optarg; - break; - case 'i': - immediate = optarg; - break; - case 'h': - print_sub_disable_help(); - return EXIT_SUCCESS; - default: - print_sub_disable_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node || !sub_name) - { - log_error("Missing required arguments: --node and --sub_name are mandatory."); - print_sub_disable_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - print_sub_disable_help(); - return EXIT_FAILURE; - } - - db = get_postgres_db(node); - if (!db) - { - log_error("Failed to get database name for node '%s'.", node); - print_sub_disable_help(); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Build the SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.sub_disable(" - "subscription_name := '%s', " - "immediate := %s);", - sub_name, - immediate ? immediate : "false"); - - /* Execute SQL query */ - res = PQexec(conn, sql); - log_debug0("SQL: %s", sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Check for NULL result */ - if (PQntuples(res) == 0 || PQgetvalue(res, 0, 0) == NULL) - { - log_error("SQL function returned NULL for query: %s", sql); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Clean up */ - PQclear(res); - PQfinish(conn); - - return EXIT_SUCCESS; -} - -int -handle_sub_show_status_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"sub_name", required_argument, 0, 's'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *sub_name = NULL; - const char *conninfo; - const char *db; - PGconn *conn; - PGresult *res; - char sql[2048]; - int option_index = 0; - int c; - int nrows; - int nfields; - - /* Parse command-line options */ - while ((c = getopt_long(argc, argv, "n:s:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - sub_name = optarg; - break; - case 'h': - print_sub_show_status_help(); - return EXIT_SUCCESS; - default: - print_sub_show_status_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node || !sub_name) - { - log_error("Missing required arguments: --node and --sub_name are mandatory."); - print_sub_show_status_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - print_sub_show_status_help(); - return EXIT_FAILURE; - } - - db = get_postgres_db(node); - if (!db) - { - log_error("Failed to get database name for node '%s'.", node); - print_sub_show_status_help(); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Build the SQL query */ - snprintf(sql, sizeof(sql), - "SELECT subscription_name, status, provider_node, provider_dsn, " - "slot_name, replication_sets, forward_origins " - "FROM spock.sub_show_status(subscription_name := '%s');", - sub_name); - - /* Execute the SQL query */ - log_debug0("SQL: %s\n", sql); - res = PQexec(conn, sql); - - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("Failed to execute query: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Print the results */ - nrows = PQntuples(res); - nfields = PQnfields(res); - - for (int i = 0; i < nrows; i++) - { - for (int j = 0; j < nfields; j++) - { - log_info("%s: %s\n", PQfname(res, j), PQgetvalue(res, i, j)); - } - } - - /* Clean up */ - PQclear(res); - PQfinish(conn); - return EXIT_SUCCESS; -} - -int -handle_sub_show_table_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"sub_name", required_argument, 0, 's'}, - {"relation", required_argument, 0, 'r'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *sub_name = NULL; - char *relation = NULL; - const char *conninfo; - const char *db; - PGconn *conn; - PGresult *res; - char sql[2048]; - int option_index = 0; - int c; - int nrows; - int nfields; - - /* Parse command-line options */ - while ((c = getopt_long(argc, argv, "n:s:r:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - sub_name = optarg; - break; - case 'r': - relation = optarg; - break; - case 'h': - print_sub_show_table_help(); - return EXIT_SUCCESS; - default: - print_sub_show_table_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node || !sub_name || !relation) - { - log_error("Missing required arguments: --node, --sub_name, and --relation are mandatory."); - print_sub_show_table_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - print_sub_show_table_help(); - return EXIT_FAILURE; - } - - db = get_postgres_db(node); - if (!db) - { - log_error("Failed to get database name for node '%s'.", node); - print_sub_show_table_help(); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Build the SQL query */ - snprintf(sql, sizeof(sql), - "SELECT nspname, relname, status " - "FROM spock.sub_show_table(" - "subscription_name := '%s', " - "relation := '%s'::regclass);", - sub_name, - relation); - - /* Execute the SQL query */ - res = PQexec(conn, sql); - log_debug0("SQL: %s", sql); - - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - log_error("SQL command failed: %s", PQerrorMessage(conn)); - PQclear(res); - PQfinish(conn); - return EXIT_FAILURE; - } - - /* Print the results */ - nrows = PQntuples(res); - nfields = PQnfields(res); - - for (int i = 0; i < nrows; i++) - { - for (int j = 0; j < nfields; j++) - { - log_info("%s: %s\n", PQfname(res, j), PQgetvalue(res, i, j)); - } - } - - /* Clean up */ - PQclear(res); - PQfinish(conn); - - return EXIT_SUCCESS; -} - -int -handle_sub_resync_table_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"sub_name", required_argument, 0, 's'}, - {"relation", required_argument, 0, 'r'}, - {"truncate", required_argument, 0, 't'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *sub_name = NULL; - char *relation = NULL; - char *truncate = NULL; - const char *conninfo; - const char *db; - PGconn *conn; - char sql[2048]; - int option_index = 0; - int c; - - /* Parse command-line options */ - while ((c = getopt_long(argc, argv, "n:s:r:t:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - sub_name = optarg; - break; - case 'r': - relation = optarg; - break; - case 't': - truncate = optarg; - break; - case 'h': - print_sub_resync_table_help(); - return EXIT_SUCCESS; - default: - print_sub_resync_table_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node || !sub_name || !relation || !truncate) - { - log_error("Missing required arguments: --node, --sub_name, --relation, and --truncate are mandatory."); - print_sub_resync_table_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - print_sub_resync_table_help(); - return EXIT_FAILURE; - } - - db = get_postgres_db(node); - if (!db) - { - log_error("Failed to get database name for node '%s'.", node); - print_sub_resync_table_help(); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Build the SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.sub_resync_table(" - "node_name := '%s', " - "subscription_name := '%s', " - "relation_name := '%s', " - "database_name := '%s', " - "truncate := %s);", - node, - sub_name, - relation, - db, - truncate); - - /* Execute the SQL query */ - log_debug0("SQL: %s\n", sql); - run_sql(conn, sql); - - /* Close the connection */ - PQfinish(conn); - - return EXIT_SUCCESS; -} - -int -handle_sub_add_repset_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"sub_name", required_argument, 0, 's'}, - {"replication_set", required_argument, 0, 'r'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *sub_name = NULL; - char *replication_set = NULL; - const char *conninfo; - const char *db; - PGconn *conn; - char sql[2048]; - int option_index = 0; - int c; - - /* Parse command-line options */ - while ((c = getopt_long(argc, argv, "n:s:r:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - sub_name = optarg; - break; - case 'r': - replication_set = optarg; - break; - case 'h': - print_sub_add_repset_help(); - return EXIT_SUCCESS; - default: - print_sub_add_repset_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node || !sub_name || !replication_set) - { - log_error("Missing required arguments: --node, --sub_name, and --replication_set are mandatory."); - print_sub_add_repset_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - print_sub_add_repset_help(); - return EXIT_FAILURE; - } - - db = get_postgres_db(node); - if (!db) - { - log_error("Failed to get database name for node '%s'.", node); - print_sub_add_repset_help(); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Build the SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.sub_add_repset(" - "subscription_name := '%s', " - "replication_set := '%s');", - sub_name, - replication_set); - - /* Execute the SQL query */ - log_debug0("SQL: %s\n", sql); - run_sql(conn, sql); - - /* Close the connection */ - PQfinish(conn); - - return EXIT_SUCCESS; -} - -int -handle_sub_remove_repset_command(int argc, char *argv[]) -{ - static struct option long_options[] = { - {"node", required_argument, 0, 'n'}, - {"sub_name", required_argument, 0, 's'}, - {"replication_set", required_argument, 0, 'r'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - char *node = NULL; - char *sub_name = NULL; - char *replication_set = NULL; - const char *conninfo; - const char *db; - PGconn *conn; - char sql[2048]; - int option_index = 0; - int c; - - /* Parse command-line options */ - while ((c = getopt_long(argc, argv, "n:s:r:h", long_options, &option_index)) != -1) - { - switch (c) - { - case 'n': - node = optarg; - break; - case 's': - sub_name = optarg; - break; - case 'r': - replication_set = optarg; - break; - case 'h': - print_sub_remove_repset_help(); - return EXIT_SUCCESS; - default: - print_sub_remove_repset_help(); - return EXIT_FAILURE; - } - } - - /* Validate required arguments */ - if (!node || !sub_name || !replication_set) - { - log_error("Missing required arguments: --node, --sub_name, and --replication_set are mandatory."); - print_sub_remove_repset_help(); - return EXIT_FAILURE; - } - - /* Get connection info */ - conninfo = get_postgres_coninfo(node); - if (conninfo == NULL) - { - log_error("Failed to get connection info for node '%s'.", node); - print_sub_remove_repset_help(); - return EXIT_FAILURE; - } - - db = get_postgres_db(node); - if (!db) - { - log_error("Failed to get database name for node '%s'.", node); - print_sub_remove_repset_help(); - return EXIT_FAILURE; - } - - /* Connect to the database */ - conn = connectdb(conninfo); - if (conn == NULL) - { - log_error("Failed to connect to the database."); - return EXIT_FAILURE; - } - - /* Build the SQL query */ - snprintf(sql, sizeof(sql), - "SELECT spock.sub_remove_repset(" - "subscription_name := '%s', " - "replication_set := '%s');", - sub_name, - replication_set); - - /* Execute the SQL query */ - log_debug0("SQL: %s\n", sql); - run_sql(conn, sql); - - /* Close the connection */ - PQfinish(conn); - - return EXIT_SUCCESS; -} - -static void -print_sub_create_help(void) -{ - printf("Usage: spockctrl sub create [OPTIONS]\n"); - printf("Create a new subscription\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --sub_name Name of the subscription (required)\n"); - printf(" --provider_dsn Provider DSN (required)\n"); - printf(" --replication_sets Replication sets (optional)\n"); - printf(" --synchronize_structure Synchronize structure (optional)\n"); - printf(" --synchronize_data Synchronize data (optional)\n"); - printf(" --forward_origins Forward origins (optional)\n"); - printf(" --apply_delay Apply delay (optional)\n"); - printf(" --enabled Enable subscription (optional)\n"); - printf(" --force_text_transfer Force text transfer (optional)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_sub_drop_help(void) -{ - printf("Usage: spockctrl sub drop [OPTIONS]\n"); - printf("Drop a subscription\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --sub_name Name of the subscription (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_sub_enable_help(void) -{ - printf("Usage: spockctrl sub enable [OPTIONS]\n"); - printf("Enable a subscription\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --sub_name Name of the subscription (required)\n"); - printf(" --immediate Immediate enable (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_sub_disable_help(void) -{ - printf("Usage: spockctrl sub disable [OPTIONS]\n"); - printf("Disable a subscription\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --sub_name Name of the subscription (required)\n"); -} - -static void -print_sub_show_status_help(void) -{ - printf("Usage: spockctrl sub show-status [OPTIONS]\n"); - printf("Show the status of a subscription\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --sub_name Name of the subscription (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_sub_show_table_help(void) -{ - printf("Usage: spockctrl sub show-table [OPTIONS]\n"); - printf("Show the table of a subscription\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --sub_name Name of the subscription (required)\n"); - printf(" --relation Relation name (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_sub_resync_table_help(void) -{ - printf("Usage: spockctrl sub resync-table [OPTIONS]\n"); - printf("Resync a table in a subscription\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --sub_name Name of the subscription (required)\n"); - printf(" --relation Relation name (required)\n"); - printf(" --truncate Truncate table before resync (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_sub_add_repset_help(void) -{ - printf("Usage: spockctrl sub add-repset [OPTIONS]\n"); - printf("Add a replication set to a subscription\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --sub_name Name of the subscription (required)\n"); - printf(" --replication_set Replication set name (required)\n"); - printf(" --help Show this help message\n"); -} - -static void -print_sub_remove_repset_help(void) -{ - printf("Usage: spockctrl sub remove-repset [OPTIONS]\n"); - printf("Remove a replication set from a subscription\n"); - printf("Options:\n"); - printf(" --node Name of the node (required)\n"); - printf(" --sub_name Name of the subscription (required)\n"); - printf(" --replication_set Replication set name (required)\n"); - printf(" --help Show this help message\n"); -} diff --git a/utils/spockctrl/src/util.c b/utils/spockctrl/src/util.c deleted file mode 100644 index 34b464c9..00000000 --- a/utils/spockctrl/src/util.c +++ /dev/null @@ -1,388 +0,0 @@ -/*------------------------------------------------------------------------- - * - * util.c - * utility functions for spockctrl - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include "util.h" -#include "logger.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -int -is_valid_json(const char *json_str) -{ - json_error_t error; - json_t *json; - - if (!json_str || strlen(json_str) == 0) - { - log_error("Invalid JSON: Input is empty or NULL."); - return 0; - } - - json = json_loads(json_str, JSON_DECODE_ANY, &error); - if (!json) - { - log_error("Invalid JSON: %s (line %d, column %d, position %d). Input: '%s'", - error.text, error.line, error.column, error.position, json_str); - return 0; - } - json_decref(json); - return 1; -} - -/* Function to parse a JSON file into a json_t object */ -json_t * -load_json_file(const char *file_path) -{ - json_error_t error; - json_t *json = json_load_file(file_path, 0, &error); - - if (!json) - { - log_error("Error loading JSON file: %s (line %d, column %d).", error.text, error.line, error.column); - return NULL; - } - return json; -} - -/* Function to parse a JSON string into a json_t object */ -json_t * -parse_json_string(const char *json_str) -{ - json_error_t error; - json_t *json = json_loads(json_str, 0, &error); - - if (!json) - { - log_error("Error parsing JSON string: %s (line %d, column %d).", error.text, error.line, error.column); - return NULL; - } - return json; -} - -/* Function to safely get a string value from a JSON object */ -char * -get_json_string_value(json_t * json, const char *key) -{ - json_t *value = json_object_get(json, key); - - if (!json_is_string(value)) - { - log_error("Error: Key '%s' is not a string in JSON object.", key); - return NULL; - } - return strdup(json_string_value(value)); -} - -/* Function to safely get an array from a JSON object */ -json_t * -get_json_array(json_t * json, const char *key) -{ - json_t *array = json_object_get(json, key); - - if (!json_is_array(array)) - { - log_error("Error: Key '%s' is not an array in JSON object.", key); - return NULL; - } - return array; -} - -/* Function to get the current timestamp */ -char * -get_current_timestamp(void) -{ - time_t now = time(NULL); - struct tm *t = localtime(&now); - static char timestamp[20]; - - strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", t); - return timestamp; -} - -/* Function to get the current user */ -char * -get_current_user(void) -{ - struct passwd *pw = getpwuid(getuid()); - - return pw ? pw->pw_name : "unknown"; -} - -/* Function to trim whitespace from a string */ -char * -trim_whitespace(char *str) -{ - char *end; - - /* Trim leading space */ - while (isspace((unsigned char) *str)) - str++; - - if (*str == 0) /* All spaces? */ - return str; - - /* Trim trailing space */ - end = str + strlen(str) - 1; - while (end > str && isspace((unsigned char) *end)) - end--; - - /* Write new null terminator */ - *(end + 1) = '\0'; - - return str; -} - -/* Function to convert a string to lowercase */ -char * -str_to_lower(char *str) -{ - for (char *p = str; *p; ++p) - { - *p = tolower((unsigned char) *p); - } - return str; -} - -/* Function to convert a string to uppercase */ -char * -str_to_upper(char *str) -{ - for (char *p = str; *p; ++p) - { - *p = toupper((unsigned char) *p); - } - return str; -} - -char * -make_sql(const char *format,...) -{ - char *sql = malloc(1024); - va_list args; - - va_start(args, format); - - if (sql == NULL) - { - va_end(args); - return NULL; - } - - vsnprintf(sql, 1024, format, args); - va_end(args); - - return sql; -} - -/* Function to create a PostgreSQL SELECT query */ -char * -make_select_query(const char *table, const char *columns, const char *condition) -{ - return make_sql("SELECT %s FROM %s WHERE %s;", columns, table, condition); -} - -/* Function to create a Spock-specific query with variable parameters */ -char * -make_spock_query(const char *command, const char *params_format,...) -{ - va_list args; - char *query = malloc(1024); - char *final_query; - - if (!query) - { - log_error("Memory allocation failed for Spock query."); - return NULL; - } - - va_start(args, params_format); - vsnprintf(query, 1024, params_format, args); - va_end(args); - - final_query = malloc(1024); - if (!final_query) - { - log_error("Memory allocation failed for final Spock query."); - free(query); - return NULL; - } - snprintf(final_query, 1024, "SELECT spock.%s(%s);", command, query); - free(query); - return final_query; -} - -void -trim_newline(char *str) -{ - char *p = strchr(str, '\n'); - - if (p) - *p = '\0'; -} - -int -get_value_from_outfile(const char *node, const char *key, char *value, size_t value_sz) -{ - char filename[256]; - FILE *f; - char line[1024]; - - snprintf(filename, sizeof(filename), "%s.out", node); - f = fopen(filename, "r"); - if (!f) - return -1; - while (fgets(line, sizeof(line), f)) - { - char *eq = strchr(line, '='); - - if (!eq) - continue; - *eq = '\0'; - trim_newline(eq + 1); - if (strcmp(line, key) == 0) - { - snprintf(value, value_sz - 1, "%s", eq + 1); - fclose(f); - return 0; - } - } - fclose(f); - return -1; -} - -#define BUF_SZ 32768 -#define NODE_MAX 128 -#define KEY_MAX 128 -#define VAL_MAX 4096 -#define ESC_MAX 8192 - -static void -escape_quotes(const char *src, char *dst, size_t dst_sz) -{ - const char *p = src; - char *d = dst; - - while (*p && (size_t) (d - dst) < dst_sz - 2) - { - if (*p == '\'') - *d++ = '\''; - *d++ = *p++; - } - *d = '\0'; -} - -char * -substitute_sql_vars(const char *sql_stmt) -{ - static char buf[BUF_SZ]; - regex_t re; - const char *pat = "\\$([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)"; - regmatch_t m[3]; - const char *src = sql_stmt; - char *dst = buf; - size_t left = sizeof(buf); - char node[NODE_MAX]; - char key[KEY_MAX]; - char val[VAL_MAX]; - char esc[ESC_MAX]; - size_t pre; - int nlen; - int klen; - int quoted; - - buf[0] = '\0'; - if (regcomp(&re, pat, REG_EXTENDED) != 0) - return NULL; - - while (*src && left > 1) - { - int rc = regexec(&re, src, 3, m, 0); - - if (rc != 0) - { - size_t rem = strlen(src); - - if (rem >= left) - rem = left - 1; - memcpy(dst, src, rem); - dst[rem] = '\0'; - break; - } - - /* copy text before match */ - pre = (size_t) m[0].rm_so; - if (pre >= left) - pre = left - 1; - memcpy(dst, src, pre); - dst += pre; - left -= pre; - - /* parse identifiers */ - nlen = m[1].rm_eo - m[1].rm_so; - klen = m[2].rm_eo - m[2].rm_so; - if (nlen >= NODE_MAX || klen >= KEY_MAX) - goto fail; - memcpy(node, src + m[1].rm_so, nlen); - node[nlen] = '\0'; - memcpy(key, src + m[2].rm_so, klen); - key[klen] = '\0'; - if (get_value_from_outfile(node, key, val, sizeof(val)) != 0) - goto fail; - escape_quotes(val, esc, sizeof(esc)); - - /* detect if placeholder is already enclosed in single quotes */ - quoted = (m[0].rm_so > 0 && src[m[0].rm_so - 1] == '\'' && - src[m[0].rm_eo] == '\''); - - if (!quoted) - { - if (left < strlen(esc) + 3) - goto fail; - *dst++ = '\''; - left--; /* opening quote */ - snprintf(dst, BUF_SZ, "%s", esc); - left -= strlen(esc); - dst += strlen(esc); - *dst++ = '\''; - left--; /* closing quote */ - src += m[0].rm_eo; /* continue after match */ - } - else - { - /* opening quote already copied in pre‑text */ - if (left <= strlen(esc) + 1) - goto fail; - strcpy(dst, esc); - left -= strlen(esc); - dst += strlen(esc); - *dst++ = '\''; - left--; /* replicate closing quote */ - src += m[0].rm_eo + 1; /* skip placeholder + quote */ - } - } - - regfree(&re); - return buf; - -fail: - regfree(&re); - return NULL; -} diff --git a/utils/spockctrl/src/workflow.c b/utils/spockctrl/src/workflow.c deleted file mode 100644 index ccdd498d..00000000 --- a/utils/spockctrl/src/workflow.c +++ /dev/null @@ -1,753 +0,0 @@ -/*------------------------------------------------------------------------- - * - * workflow.c - * workflow execution and step parsing functions for spockctrl - * - * Copyright (c) 2022-2026, pgEdge, Inc. - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, The Regents of the University of California - * - *------------------------------------------------------------------------- - */ - -#include -#include -#include -#include -#include -#include "workflow.h" -#include "node.h" -#include "sub.h" -#include "logger.h" -#include "util.h" -#include "dbconn.h" -#include "slot.h" -#include "spock.h" -#include "repset.h" -#include "sql.h" - -/* Function declarations */ -static int prepare_arguments(Step *step, char *argv[], int max_args, const char *default_db); -static int parse_step(json_t *step_json, Step *step); -static int parse_spock_step(json_t *spock, Step *step); -static int parse_sql_step(json_t *sql, Step *step); -static int parse_shell_step(json_t *shell, Step *step); -static int parse_steps(json_t *steps_json, Step **steps, int *step_count); -static int parse_success_failure_steps(json_t *json, Step *success_step, Step *failure_step); -static int execute_step(Step *step, int step_index); - -int -handle_spock_command(Step *step); - -static int prepare_arguments(Step *step, char *argv[], int max_args, const char *default_db) -{ - int argc = 0; - - if (argc >= max_args - 1) return -1; - argv[argc++] = "spockctrl"; - - if (argc >= max_args - 1) return -1; - argv[argc] = malloc(256); - if (argv[argc] == NULL) - { - log_error("Error: could not allocate memory for argument"); - return -1; - } - snprintf(argv[argc++], 256, "--node=%s", step->node ? step->node : ""); - - for (int i = 0; i < MAX_ARGS && step->args[i] != NULL; i++) - { - if (argc >= max_args - 1) - { - log_error("Too many arguments in step: %s", step->name ? step->name : "(unknown)"); - break; // don't overflow argv - } - argv[argc++] = step->args[i]; - } - /* Add the ignore_errors flag if set */ - if (step->ignore_errors) - { - if (argc >= max_args - 1) - { - log_error("Too many arguments in step: %s", step->name ? step->name : "(unknown)"); - return -1; - } - argv[argc++] = "--ignore-errors"; - } - - argv[argc] = NULL; /* Null-terminate */ - return argc; -} - -Workflow * -load_workflow(const char *json_file_path) -{ - Workflow *workflow; - json_t *json; - json_t *steps; - - /* Load the JSON file */ - json = load_json_file(json_file_path); - if (!json) - { - return NULL; - } - - /* Allocate memory for the workflow structure */ - workflow = (Workflow *) malloc(sizeof(Workflow)); - if (workflow == NULL) - { - log_error("Error: could not allocate memory for workflow"); - json_decref(json); - return NULL; - } - - /* Get the workflow name */ - workflow->workflow_name = get_json_string_value(json, "workflow_name"); - if (!workflow->workflow_name) - { - json_decref(json); - free(workflow); - return NULL; - } - - /* Get the description */ - workflow->description = get_json_string_value(json, "description"); - if (!workflow->description) - { - json_decref(json); - free(workflow->workflow_name); - free(workflow); - return NULL; - } - - /* Get the steps */ - steps = get_json_array(json, "steps"); - if (!steps) - { - json_decref(json); - free(workflow->workflow_name); - free(workflow->description); - free(workflow); - return NULL; - } - - if (parse_steps(steps, &workflow->steps, &workflow->step_count) != 0) - { - json_decref(json); - free(workflow->workflow_name); - free(workflow->description); - free(workflow); - return NULL; - } - - if (parse_success_failure_steps(json, &workflow->success_step, &workflow->failure_step) != 0) - { - json_decref(json); - free_workflow(workflow); - return NULL; - } - - json_decref(json); - return workflow; -} - -static int -parse_spock_step(json_t *spock, Step *step) -{ - json_t *command, *description, *args, *node, *name, *sleep_val, *ignore_errors; - int j; - - step->type = STEP_TYPE_SPOCK; - - /* Get the step name */ - name = json_object_get(spock, "name"); - if (name && json_is_string(name)) - { - step->name = strdup(json_string_value(name)); - } - else - { - step->name = NULL; /* Default to NULL if not provided */ - } - - /* Get the step command */ - command = json_object_get(spock, "command"); - if (!json_is_string(command)) - { - log_error("Error: spock step command is not a string"); - return -1; - } - step->command = strdup(json_string_value(command)); - - /* Get the step description */ - description = json_object_get(spock, "description"); - if (!json_is_string(description)) - { - log_error("Error: spock step description is not a string"); - return -1; - } - step->description = strdup(json_string_value(description)); - - /* Get the step args */ - args = json_object_get(spock, "args"); - if (!json_is_array(args)) - { - log_error("Error: spock step args is not an array"); - return -1; - } - for (j = 0; j < MAX_ARGS; j++) - { - json_t *arg = json_array_get(args, j); - step->args[j] = arg ? strdup(json_string_value(arg)) : NULL; - } - - /* Get the node field */ - node = json_object_get(spock, "node"); - if (node && json_is_string(node)) - { - step->node = strdup(json_string_value(node)); - } - else - { - step->node = NULL; /* Default to NULL if not provided */ - } - - /* Get the sleep field */ - sleep_val = json_object_get(spock, "sleep"); - if (sleep_val && json_is_integer(sleep_val)) - step->sleep = (int)json_integer_value(sleep_val); - else - step->sleep = 0; - - /* Parse ignore_errors flag */ - ignore_errors = json_object_get(spock, "ignore_errors"); - if (ignore_errors && json_is_boolean(ignore_errors)) - { - step->ignore_errors = json_boolean_value(ignore_errors); - } - else - { - step->ignore_errors = false; // Default to false - } - - return 0; -} - -static int -parse_sql_step(json_t *sql, Step *step) -{ - json_t *command, *description, *args, *node, *name, *sleep_val, *ignore_errors; - int j; - - step->type = STEP_TYPE_SQL; - - /* Get the step name */ - name = json_object_get(sql, "name"); - if (name && json_is_string(name)) - { - step->name = strdup(json_string_value(name)); - } - else - { - step->name = NULL; /* Default to NULL if not provided */ - } - - /* Get the step command */ - command = json_object_get(sql, "command"); - if (!json_is_string(command)) - { - log_error("Error: sql step command is not a string"); - return -1; - } - step->command = strdup(json_string_value(command)); - - /* Get the step description */ - description = json_object_get(sql, "description"); - if (!json_is_string(description)) - { - log_error("Error: sql step description is not a string"); - return -1; - } - step->description = strdup(json_string_value(description)); - - /* Get the step args */ - args = json_object_get(sql, "args"); - if (!json_is_array(args)) - { - log_error("Error: sql step args is not an array"); - return -1; - } - for (j = 0; j < MAX_ARGS; j++) - { - json_t *arg = json_array_get(args, j); - step->args[j] = arg ? strdup(json_string_value(arg)) : NULL; - } - - /* Get the node field */ - node = json_object_get(sql, "node"); - if (node && json_is_string(node)) - { - step->node = strdup(json_string_value(node)); - } - else - { - step->node = NULL; /* Default to NULL if not provided */ - } - - /* Get the sleep field */ - sleep_val = json_object_get(sql, "sleep"); - if (sleep_val && json_is_integer(sleep_val)) - step->sleep = (int)json_integer_value(sleep_val); - else - step->sleep = 0; - - ignore_errors = json_object_get(sql, "ignore_errors"); - if (ignore_errors && json_is_boolean(ignore_errors)) - { - step->ignore_errors = json_boolean_value(ignore_errors); - } - else - { - step->ignore_errors = false; // Default to false - } - - return 0; -} - -static int -parse_shell_step(json_t *shell, Step *step) -{ - json_t *command, *description, *name, *sleep_val; - - step->type = STEP_TYPE_SHELL; - - /* Get the step name */ - name = json_object_get(shell, "name"); - if (name && json_is_string(name)) - { - step->name = strdup(json_string_value(name)); - } - else - { - step->name = NULL; /* Default to NULL if not provided */ - } - - /* Get the shell command */ - command = json_object_get(shell, "command"); - if (!json_is_string(command)) - { - log_error("Error: shell step command is not a string"); - return -1; - } - step->command = strdup(json_string_value(command)); - - /* Get the step description */ - description = json_object_get(shell, "description"); - if (!json_is_string(description)) - { - log_error("Error: shell step description is not a string"); - return -1; - } - step->description = strdup(json_string_value(description)); - - /* Get the sleep field */ - sleep_val = json_object_get(shell, "sleep"); - if (sleep_val && json_is_integer(sleep_val)) - step->sleep = (int)json_integer_value(sleep_val); - else - step->sleep = 0; - - return 0; -} - -static int -parse_step(json_t *step_json, Step *step) -{ - json_t *spock, *sql, *shell, *on_success, *on_failure; - - /* Check for spock step */ - spock = json_object_get(step_json, "spock"); - if (spock && json_is_object(spock)) - { - if (parse_spock_step(spock, step) != 0) - return -1; - } - /* Check for sql step */ - else if ((sql = json_object_get(step_json, "sql")) && json_is_object(sql)) - { - if (parse_sql_step(sql, step) != 0) - return -1; - } - /* Check for shell step */ - else if ((shell = json_object_get(step_json, "shell")) && json_is_object(shell)) - { - if (parse_shell_step(shell, step) != 0) - return -1; - } - else - { - log_error("Error: unknown step type"); - return -1; - } - - /* Get the step on_success */ - on_success = json_object_get(step_json, "on_success"); - if (on_success && json_is_object(on_success)) - { - step->on_success = strdup(json_dumps(on_success, JSON_COMPACT)); - } - else - { - step->on_success = NULL; /* Default to NULL if not provided or invalid */ - } - - /* Get the step on_failure */ - on_failure = json_object_get(step_json, "on_failure"); - if (on_failure && json_is_object(on_failure)) - { - step->on_failure = strdup(json_dumps(on_failure, JSON_COMPACT)); - } - else - { - step->on_failure = NULL; /* Default to NULL if not provided or invalid */ - } - - return 0; -} - -static int -parse_steps(json_t *steps_json, Step **steps, int *step_count) -{ - int i; - - *step_count = json_array_size(steps_json); - *steps = (Step *) malloc(*step_count * sizeof(Step)); - if (*steps == NULL) - { - log_error("Error: could not allocate memory for steps"); - return -1; - } - - for (i = 0; i < *step_count; i++) - { - json_t *step_json = json_array_get(steps_json, i); - if (parse_step(step_json, &(*steps)[i]) != 0) - { - return -1; - } - } - - return 0; -} - -static int -parse_success_failure_steps(json_t *json, Step *success_step, Step *failure_step) -{ - json_t *success_step_json; - json_t *failure_step_json; - int j; - - success_step_json = json_object_get(json, "success_step"); - if (success_step_json && json_is_object(success_step_json) && json_object_size(success_step_json) > 0) - { - success_step->name = strdup(json_string_value(json_object_get(success_step_json, "name"))); - success_step->description = strdup(json_string_value(json_object_get(success_step_json, "description"))); - success_step->command = strdup(json_string_value(json_object_get(success_step_json, "command"))); - for (j = 0; j < MAX_ARGS; j++) - { - json_t *arg = json_array_get(json_object_get(success_step_json, "args"), j); - success_step->args[j] = arg ? strdup(json_string_value(arg)) : NULL; - } - for (j = 0; j < MAX_OPTIONS; j++) - { - json_t *option = json_array_get(json_object_get(success_step_json, "options"), j); - success_step->options[j] = option ? strdup(json_string_value(option)) : NULL; - } - } - else - { - /* Initialize success_step as empty */ - success_step->name = NULL; - success_step->description = NULL; - success_step->command = NULL; - for (j = 0; j < MAX_ARGS; j++) - { - success_step->args[j] = NULL; - } - for (j = 0; j < MAX_OPTIONS; j++) - { - success_step->options[j] = NULL; - } - } - - failure_step_json = json_object_get(json, "failure_step"); - if (failure_step_json && json_is_object(failure_step_json) && json_object_size(failure_step_json) > 0) - { - failure_step->name = strdup(json_string_value(json_object_get(failure_step_json, "name"))); - failure_step->description = strdup(json_string_value(json_object_get(failure_step_json, "description"))); - failure_step->command = strdup(json_string_value(json_object_get(failure_step_json, "command"))); - for (j = 0; j < MAX_ARGS; j++) - { - json_t *arg = json_array_get(json_object_get(failure_step_json, "args"), j); - failure_step->args[j] = arg ? strdup(json_string_value(arg)) : NULL; - } - for (j = 0; j < MAX_OPTIONS; j++) - { - json_t *option = json_array_get(json_object_get(failure_step_json, "options"), j); - failure_step->options[j] = option ? strdup(json_string_value(option)) : NULL; - } - } - else - { - /* Initialize failure_step as empty */ - failure_step->name = NULL; - failure_step->description = NULL; - failure_step->command = NULL; - for (j = 0; j < MAX_ARGS; j++) - { - failure_step->args[j] = NULL; - } - for (j = 0; j < MAX_OPTIONS; j++) - { - failure_step->options[j] = NULL; - } - } - - return 0; -} - -void -free_workflow(Workflow *workflow) -{ - int i, j; - - free(workflow->workflow_name); - free(workflow->description); - - for (i = 0; i < workflow->step_count; i++) - { - Step *step = &workflow->steps[i]; - free(step->name); - free(step->description); - free(step->command); - for (j = 0; j < MAX_ARGS; j++) - { - free(step->args[j]); - } - for (j = 0; j < MAX_OPTIONS; j++) - { - free(step->options[j]); - } - free(step->on_success); - free(step->on_failure); - } - free(workflow->steps); - - free(workflow->success_step.name); - free(workflow->success_step.description); - free(workflow->success_step.command); - for (j = 0; j < MAX_ARGS; j++) - { - free(workflow->success_step.args[j]); - } - for (j = 0; j < MAX_OPTIONS; j++) - { - free(workflow->success_step.options[j]); - } - - free(workflow->failure_step.name); - free(workflow->failure_step.description); - free(workflow->failure_step.command); - for (j = 0; j < MAX_ARGS; j++) - { - free(workflow->failure_step.args[j]); - } - for (j = 0; j < MAX_OPTIONS; j++) - { - free(workflow->failure_step.options[j]); - } -} - -int -run_workflow(Workflow *workflow) -{ - int i; - int step_result; - - for (i = 0; i < workflow->step_count; i++) - { - Step *step = &workflow->steps[i]; - - /* Print step description before execution */ - printf("[%s] [Step - %02d] %s ", get_current_timestamp(), i + 1, step->description); - fflush(stdout); - - step_result = execute_step(step, i); - - /* Print result on the same line */ - if (step_result == 0) - { - printf("[OK]\n"); - } - else - { - printf("[FAILED]\n"); - //return -1; /* Stop execution on failure */ - } - - /* Handle on_success and on_failure (currently placeholders) */ - if (step->on_success && strlen(step->on_success) > 0) - { - log_info("On success: %s", step->on_success); - } - if (step->on_failure && strlen(step->on_failure) > 0) - { - log_info("On failure: %s", step->on_failure); - } - - /* Sleep after step if requested */ - if (step->sleep > 0) - { - log_info("Sleeping for %d seconds after step %d...", step->sleep, i + 1); - sleep(step->sleep); - } - } - - return 0; -} - -static int -handle_sql_command(Step *step) -{ - char *argv[MAX_ARGS + 1]; - int argc; - int result; - - /* Prepare arguments for the command */ - if ((argc = prepare_arguments(step, argv, MAX_ARGS + 1, NULL)) < 0) - { - log_error("Failed to prepare arguments"); - return -1; - } - - /* Debug: Print the prepared arguments */ - for (int i = 0; i < argc; i++) - { - log_debug0("Argument[%d]: %s", i, argv[i]); - } - - /* Execute the SQL command */ - result = handle_sql_exec_command(argc, argv); - - /* Handle ignore_errors flag */ - if (result != 0) - { - log_warning("SQL command failed, but ignoring errors as per configuration."); - return 0; // Treat as success - } - - return result; -} - -static int -execute_step(Step *step, int step_index) -{ - /* Prepare arguments for the command */ - if (step->type == STEP_TYPE_SPOCK) - { - return handle_spock_command(step); - } - else if (step->type == STEP_TYPE_SQL) - { - return handle_sql_command(step); - } - else if (step->type == STEP_TYPE_SHELL) - { - //return handle_shell_command(argc, argv); - } - return 0; -} -int -handle_spock_command(Step *step) -{ - char *argv[MAX_ARGS + 1]; - int argc; - - /* Prepare arguments for the command */ - if ((argc = prepare_arguments(step, argv, MAX_ARGS + 1, NULL)) < 0) - { - log_error("Failed to prepare arguments"); - return -1; - } - - /* Debug: Print the prepared arguments */ - for (int i = 0; i < argc; i++) - { - log_debug0("Argument[%d]: %s", i, argv[i]); - } - - /* Handle specific commands */ - if (strcmp(step->command, "CREATE NODE") == 0) - { - return handle_node_create_command(argc, argv); - } - else if (strcmp(step->command, "CREATE SUBSCRIPTION") == 0) - { - return handle_sub_create_command(argc, argv); - } - else if (strcmp(step->command, "DROP SUBSCRIPTION") == 0) - { - return handle_sub_drop_command(argc, argv); - } - else if (strcmp(step->command, "DROP NODE") == 0) - { - return handle_node_drop_command(argc, argv); - } - else if (strcmp(step->command, "CREATE REPSET") == 0) - { - return handle_repset_create_command(argc, argv); - } - else if (strcmp(step->command, "DROP REPSET") == 0) - { - return handle_repset_drop_command(argc, argv); - } - else if (strcmp(step->command, "CREATE SLOT") == 0) - { - return handle_slot_create_command(argc, argv); - } - else if (strcmp(step->command, "DROP SLOT") == 0) - { - return handle_slot_drop_command(argc, argv); - } - else if (strcmp(step->command, "ENABLE SLOT") == 0) - { - return handle_slot_enable_command(argc, argv); - } - else if (strcmp(step->command, "DISABLE SLOT") == 0) - { - return handle_slot_disable_command(argc, argv); - } - else if (strcmp(step->command, "ENABLE SUBSCRIPTION") == 0) - { - return handle_sub_enable_command(argc, argv); - } - else if (strcmp(step->command, "DISABLE SUBSCRIPTION") == 0) - { - return handle_sub_disable_command(argc, argv); - } - else if (strcmp(step->command, "SHOW SUBSCRIPTION STATUS") == 0) - { - return handle_sub_show_status_command(argc, argv); - } - else if (strcmp(step->command, "SHOW SUBSCRIPTION TABLE") == 0) - { - return handle_sub_show_table_command(argc, argv); - } - else - { - log_error("Unknown command: %s", step->command); - return -1; - } - return 0; -} diff --git a/utils/spockctrl/workflows/add_4th_node.json b/utils/spockctrl/workflows/add_4th_node.json deleted file mode 100644 index 7167a541..00000000 --- a/utils/spockctrl/workflows/add_4th_node.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "workflow_name": "Add Fourth Node", - "description": "Adding fourth node (n4) to three node (n1,n2,n3) cluster.", - "steps": [ - { - "spock": { - "node": "n4", - "command": "CREATE NODE", - "description": "Create a spock node n4", - "args": [ - "--node_name=n4", - "--dsn=host=127.0.0.1 port=5435 user=lcuser dbname=pgedge password=pgedge", - "--location=Chicago", - "--country=USA", - "--info={\"key\": \"value\"}" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n4", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n2_n4) on (n4) for n2->n4", - "sleep": 5, - "args": [ - "--sub_name=sub_n2_n4", - "--provider_dsn=host=127.0.0.1 port=5433 user=lcuser dbname=pgedge password=pgedge", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=false" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n2", - "command": "CREATE SLOT", - "description": "Create a logical replication slot spk_pgedge_n2_sub_n2_n4 on (n2)", - "args": [ - "--slot=spk_pgedge_n2_sub_n2_n4", - "--plugin=spock_output" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n4", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n3_n4) on (n4) for n3->n4", - "sleep": 5, - "args": [ - "--sub_name=sub_n3_n4", - "--provider_dsn=host=127.0.0.1 port=5434 user=lcuser dbname=pgedge password=pgedge", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=false" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n3", - "command": "CREATE SLOT", - "description": "Create a logical replication slot spk_pgedge_n3_sub_n3_n4 on (n3)", - "args": [ - "--slot=spk_pgedge_n3_sub_n3_n4", - "--plugin=spock_output" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Trigger a sync event on (n3)", - "sleep": 10, - "args": [ - "--sql=SELECT spock.sync_event();" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Wait for a sync event on (n1) for n3-n1", - "sleep": 0, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n1', '$n3.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n2", - "command": "SQL", - "description": "Trigger a sync event on (n2)", - "sleep": 10, - "args": [ - "--sql=SELECT spock.sync_event();" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Wait for a sync event on (n1) for n1-n2", - "sleep": 0, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n2', '$n2.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n4", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n1_n4) on (n4) for n1->n4", - "sleep": 0, - "args": [ - "--sub_name=sub_n1_n4", - "--provider_dsn=host=127.0.0.1 port=5432 user=lcuser dbname=pgedge password=pgedge", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=true", - "--synchronize_data=true", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "on_success": {}, - "on_failure": {} - } - }, - - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Trigger a sync event on (n1)", - "sleep": 10, - "args": [ - "--sql=SELECT spock.sync_event();" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n4", - "command": "SQL", - "description": "Wait for a sync event on (n4) for n1-n4", - "sleep": 0, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n4', '$n1.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Wait for a sync event on (n3) for n1-n3", - "sleep": 0, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n1', '$n1.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n4", - "command": "SQL", - "description": "Check commit timestamp for n4 lag from n1", - "sleep": 1, - "args": [ - "--sql=SELECT commit_timestamp FROM spock.lag_tracker WHERE origin_name = 'n1' AND receiver_name = 'n4'" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Advance the replication slot for n1->n4 based on a specific commit timestamp", - "sleep": 0, - "args": [ - "--sql=WITH lsn_cte AS (SELECT spock.get_lsn_from_commit_ts('spk_pgedge_n1_sub_n1_n4', '$n4.commit_timestamp'::timestamp) AS lsn) SELECT pg_replication_slot_advance('spk_pgedge_n1_sub_n1_n4', lsn) FROM lsn_cte;" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n1", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n4_n1) on (n1) for n4->n1", - "sleep": 0, - "args": [ - "--sub_name=sub_n4_n1", - "--provider_dsn=host=127.0.0.1 port=5435 user=lcuser dbname=pgedge password=pgedge", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "ignore_errors": false, - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n2", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n4_n2) on (n2) for n4->n2", - "sleep": 0, - "args": [ - "--sub_name=sub_n4_n2", - "--provider_dsn=host=127.0.0.1 port=5435 user=lcuser dbname=pgedge password=pgedge", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "ignore_errors": false, - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n3", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n4_n3) on (n3) for n4->n3", - "sleep": 0, - "args": [ - "--sub_name=sub_n4_n3", - "--provider_dsn=host=127.0.0.1 port=5435 user=lcuser dbname=pgedge password=pgedge", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "ignore_errors": false, - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n4", - "command": "ENABLE SUBSCRIPTION", - "description": "Enable subscription (sub_n2_n4) on n4", - "args": [ - "--sub_name=sub_n2_n4", - "--immediate=true" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n4", - "command": "ENABLE SUBSCRIPTION", - "description": "Enable subscription (sub_n3_n4) on n4", - "args": [ - "--sub_name=sub_n3_n4", - "--immediate=true" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n4", - "command": "SQL", - "description": "Monitor replication lag for all connections to n4", - "sleep": 0, - "args": [ - "--sql=DO $$\nDECLARE\n lag_n1_n4 interval;\n lag_n2_n4 interval;\n lag_n3_n4 interval;\nBEGIN\n LOOP\n SELECT now() - commit_timestamp INTO lag_n1_n4\n FROM spock.lag_tracker\n WHERE origin_name = 'n1' AND receiver_name = 'n4';\n\n SELECT now() - commit_timestamp INTO lag_n2_n4\n FROM spock.lag_tracker\n WHERE origin_name = 'n2' AND receiver_name = 'n4';\n\n SELECT now() - commit_timestamp INTO lag_n3_n4\n FROM spock.lag_tracker\n WHERE origin_name = 'n3' AND receiver_name = 'n4';\n\n RAISE NOTICE 'n1 → n4 lag: %, n2 → n4 lag: %, n3 → n4 lag: %',\n COALESCE(lag_n1_n4::text, 'NULL'),\n COALESCE(lag_n2_n4::text, 'NULL'),\n COALESCE(lag_n3_n4::text, 'NULL');\n\n EXIT WHEN lag_n1_n4 IS NOT NULL AND lag_n2_n4 IS NOT NULL AND lag_n3_n4 IS NOT NULL\n AND extract(epoch FROM lag_n1_n4) < 59\n AND extract(epoch FROM lag_n2_n4) < 59\n AND extract(epoch FROM lag_n3_n4) < 59;\n\n PERFORM pg_sleep(1);\n END LOOP;\nEND\n$$;\n" - ], - "on_success": {}, - "on_failure": {} - } - } - ] -} - - - - - \ No newline at end of file diff --git a/utils/spockctrl/workflows/add_node.json b/utils/spockctrl/workflows/add_node.json deleted file mode 100644 index e9a7ec91..00000000 --- a/utils/spockctrl/workflows/add_node.json +++ /dev/null @@ -1,231 +0,0 @@ -{ - "workflow_name": "Add Node", - "description": "Adding third node (n3) to two node (n1,n2) cluster.", - "steps": [ - { - "spock": { - "node": "n3", - "command": "CREATE NODE", - "description": "Create a spock node n3", - "args": [ - "--node_name=n3", - "--dsn=host=127.0.0.1 dbname=pgedge port=5433 user=pgedge password=pgedge", - "--location=Los Angeles", - "--country=USA", - "--info={\"key\": \"value\"}" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n3", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n2_n3) on (n3) for n2->n3", - "sleep": 5, - "args": [ - "--sub_name=sub_n2_n3", - "--provider_dsn=host=127.0.0.1 dbname=pgedge port=5432 user=pgedge password=spockpass", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=false" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n2", - "command": "CREATE SLOT", - "description": "Create a logical replication slot spk_pgedge_n2_sub_n2_n3 on (n2)", - "args": [ - "--slot=spk_pgedge_n2_sub_n2_n3", - "--plugin=spock_output" - ], - "on_success": {}, - "on_failure": {} - } - }, - - { - "sql": { - "node": "n2", - "command": "SQL", - "description": "Trigger a sync event on (n2)", - "sleep": 10, - "ignore_errors": false, - "args": [ - "--sql=SELECT spock.sync_event();" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Wait for a sync event on (n1) for n2-n1", - "sleep": 0, - "ignore_errors": false, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n2', '$n2.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n3", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n1_n3) on n3 for n1->n3", - "sleep": 0, - "args": [ - "--sub_name=sub_n1_n3", - "--provider_dsn=host=127.0.0.1 dbname=pgedge port=5431 user=pgedge password=spockpass", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=true", - "--synchronize_data=true", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n1", - "command": "SQL", - "description": "Trigger a sync event on (n1)", - "sleep": 0, - "ignore_errors": false, - "args": [ - "--sql=SELECT spock.sync_event();" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Wait for a sync event on (n1) for n1-n3", - "sleep": 0, - "ignore_errors": false, - "args": [ - "--sql=CALL spock.wait_for_sync_event(true, 'n1', '$n1.sync_event'::pg_lsn, 1200000);" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Check commit timestamp for n3 lag", - "sleep": 0, - "ignore_errors": false, - "args": [ - "--sql=SELECT commit_timestamp FROM spock.lag_tracker WHERE origin_name = 'n2' AND receiver_name = 'n3'" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n2", - "command": "SQL", - "description": "Advance the replication slot for n2->n3 based on a specific commit timestamp", - "sleep": 0, - "ignore_errors": true, - "args": [ - "--sql=WITH lsn_cte AS (SELECT spock.get_lsn_from_commit_ts('spk_pgedge_n2_sub_n2_n3', '$n3.commit_timestamp'::timestamp) AS lsn) SELECT pg_replication_slot_advance('spk_pgedge_n2_sub_n2_n3', lsn) FROM lsn_cte;" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n3", - "command": "ENABLE SUBSCRIPTION", - "description": "Enable subscription (sub_n2_n3) on n3", - "args": [ - "--sub_name=sub_n2_n3", - "--immediate=true" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n1", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n3_n1) on (n1) for n3->n1", - "sleep": 0, - "args": [ - "--sub_name=sub_n3_n1", - "--provider_dsn=host=127.0.0.1 dbname=pgedge port=5433 user=pgedge password=spockpass", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n2", - "command": "CREATE SUBSCRIPTION", - "description": "Create a subscription (sub_n3_n2) on (n2) for n3->n2", - "sleep": 0, - "args": [ - "--sub_name=sub_n3_n2", - "--provider_dsn=host=127.0.0.1 dbname=pgedge port=5433 user=pgedge password=spockpass", - "--replication_sets=ARRAY['default', 'default_insert_only', 'ddl_sql']", - "--synchronize_structure=false", - "--synchronize_data=false", - "--forward_origins='{}'::text[]", - "--apply_delay='0'::interval", - "--force_text_transfer=false", - "--enabled=true" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "sql": { - "node": "n3", - "command": "SQL", - "description": "Check the replication lags between nodes.", - "sleep": 0, - "ignore_errors": false, - "args": [ - "--sql=DO $$\nDECLARE\n lag_n1_n3 interval;\n lag_n2_n3 interval;\nBEGIN\n LOOP\n SELECT now() - commit_timestamp INTO lag_n1_n3\n FROM spock.lag_tracker\n WHERE origin_name = 'n1' AND receiver_name = 'n3';\n\n SELECT now() - commit_timestamp INTO lag_n2_n3\n FROM spock.lag_tracker\n WHERE origin_name = 'n2' AND receiver_name = 'n3';\n\n RAISE NOTICE 'n1 → n3 lag: %, n2 → n3 lag: %',\n COALESCE(lag_n1_n3::text, 'NULL'),\n COALESCE(lag_n2_n3::text, 'NULL');\n\n EXIT WHEN lag_n1_n3 IS NOT NULL AND lag_n2_n3 IS NOT NULL\n AND extract(epoch FROM lag_n1_n3) < 59\n AND extract(epoch FROM lag_n2_n3) < 59;\n\n PERFORM pg_sleep(1);\n END LOOP;\nEND\n$$;\n" - ], - "on_success": {}, - "on_failure": {} - } - } - ] -} diff --git a/utils/spockctrl/workflows/remove_node.json b/utils/spockctrl/workflows/remove_node.json deleted file mode 100644 index 0ebf4d64..00000000 --- a/utils/spockctrl/workflows/remove_node.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "workflow_name": "Remove Node", - "description": "Remove a spock node (n3) and its subscriptions from the cluster.", - "steps": [ - { - "spock": { - "node": "n3", - "command": "DROP SUBSCRIPTION", - "description": "Drop subscription (sub_n2_n3) on n3", - "args": [ - "--sub_name=sub_n2_n3" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n3", - "command": "DROP SUBSCRIPTION", - "description": "Drop subscription (sub_n1_n3) on n3", - "args": [ - "--sub_name=sub_n1_n3" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n1", - "command": "DROP SUBSCRIPTION", - "description": "Drop subscription (sub_n3_n1) on n1", - "args": [ - "--sub_name=sub_n3_n1" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n2", - "command": "DROP SUBSCRIPTION", - "description": "Drop subscription (sub_n3_n2) on n2", - "args": [ - "--sub_name=sub_n3_n2" - ], - "on_success": {}, - "on_failure": {} - } - }, - { - "spock": { - "node": "n3", - "command": "DROP NODE", - "description": "Drop spock node n3", - "args": [ - "--node_name=n3" - ], - "on_success": {}, - "on_failure": {} - } - } - ] -} \ No newline at end of file From 2d91ab36ae223f392960dd09590f214cb109eccb Mon Sep 17 00:00:00 2001 From: Ibrar Ahmed Date: Wed, 28 Jan 2026 14:04:52 +0300 Subject: [PATCH 2/2] SPOC381: Keep the history of SpockCtrl in docs/spock_release_notes. --- docs/spock_release_notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/spock_release_notes.md b/docs/spock_release_notes.md index 6fb67af6..28b9ae4e 100644 --- a/docs/spock_release_notes.md +++ b/docs/spock_release_notes.md @@ -65,6 +65,12 @@ Now no restriction exists. Spock will use memory until memory is exhausted (impr * Exception handling performance improvements are now managed with the spock.exception_replay_queue_size GUC. * Previously, replication lag was estimated on the source node; this meant that if there were no transactions being replicated, the reported lag could continue to increase. Lag tracking is now calculated at the target node, with improved accuracy. * Spock 5.0 implements LSN Checkpointing with `spock.sync()` and `spock.wait_for_sync_event()`. This feature allows you to identify a checkpoint in the source node WAL files, and watch for the LSN of the checkpoint on a replica node. This allows you to guarantee that a DDL change, has replicated from the source node to all other nodes before publishing an update. +* The `spockctrl` command line utility and sample workflows simplify the management of a Spock multi-master replication setup for PostgreSQL. `spockctrl` provides a convenient interface for: + * node management + * replication set management + * subscription management + * ad-hoc SQL execution + * workflow automation * Previously, replicated `DELETE` statements that attempted to delete a *missing* row were logged as exceptions. Since the purpose of a `DELETE` statement is to remove a row, we no longer log these as exceptions. Instead these are now logged in the `Resolutions` table. * `INSERT` conflicts resulting from a duplicate primary key or identity replica are now transformed into an `UPDATE` that updates all columns of the existing row, using Last-Write-Wins (LWW) logic. The transaction is then logged in the node’s `Resolutions` table, as either: * `keep local` if the local node’s `INSERT` has a later timestamp than the arriving `INSERT`