diff --git a/.github/workflows/gpsc-ci.yaml b/.github/workflows/gpsc-ci.yaml new file mode 100644 index 00000000000..87e2df5081b --- /dev/null +++ b/.github/workflows/gpsc-ci.yaml @@ -0,0 +1,316 @@ +# -------------------------------------------------------------------- +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed +# with this work for additional information regarding copyright +# ownership. The ASF licenses this file to You under the Apache +# License, Version 2.0 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of the +# License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. +# +# -------------------------------------------------------------------- +# gp_stats_collector CI Workflow +# +# Builds Cloudberry with --with-gp-stats-collector (the default in +# configure-cloudberry.sh), stands up a demo cluster with the extension +# preloaded, and runs the gp_stats_collector regression suites. +# +# Scoped to changes that can affect the extension or the core patches it +# depends on, so it does not run on every unrelated push. +# -------------------------------------------------------------------- +name: GPSC CI Pipeline + +on: + push: + branches: + - 'pgqs-**' + pull_request: + paths: + - 'gpcontrib/gp_stats_collector/**' + - '.github/workflows/gpsc-ci.yaml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + test-gpsc: + name: Build and Test gp_stats_collector (${{ matrix.os }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu22.04 + image: apache/incubator-cloudberry:cbdb-build-ubuntu22.04-latest + - os: rocky8 + image: apache/incubator-cloudberry:cbdb-build-rocky8-latest + - os: rocky9 + image: apache/incubator-cloudberry:cbdb-build-rocky9-latest + container: + image: ${{ matrix.image }} + options: >- + --user root + -h cdw + + steps: + - name: Checkout Cloudberry source + uses: actions/checkout@v4 + with: + # Init/build scripts hardcode a "cloudberry" source dir name + # (e.g. create-cloudberry-demo-cluster.sh uses ${SRC_DIR}/../cloudberry), + # so the checkout path must be "cloudberry", not the repo name. + path: cloudberry + submodules: recursive + + - name: Cloudberry Environment Initialization + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + if ! su - gpadmin -c "/tmp/init_system.sh"; then + echo "::error::Container initialization failed" + exit 1 + fi + mkdir -p "${SRC_DIR}/build-logs" + chown -R gpadmin:gpadmin . + chmod -R 755 . + + - name: Configure + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + chmod +x "${SRC_DIR}"/devops/build/automation/cloudberry/scripts/configure-cloudberry.sh + if ! time su - gpadmin -c "cd ${SRC_DIR} && SRC_DIR=${SRC_DIR} ${SRC_DIR}/devops/build/automation/cloudberry/scripts/configure-cloudberry.sh"; then + echo "::error::Configure failed" + exit 1 + fi + + - name: Build + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + chmod +x "${SRC_DIR}"/devops/build/automation/cloudberry/scripts/build-cloudberry.sh + if ! time su - gpadmin -c "cd ${SRC_DIR} && SRC_DIR=${SRC_DIR} ${SRC_DIR}/devops/build/automation/cloudberry/scripts/build-cloudberry.sh"; then + echo "::error::Build failed" + exit 1 + fi + + - name: Create demo cluster (gp_stats_collector preloaded) + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + chmod +x "${SRC_DIR}"/devops/build/automation/cloudberry/scripts/create-cloudberry-demo-cluster.sh + if ! time su - gpadmin -c "cd ${SRC_DIR} && SRC_DIR=${SRC_DIR} ${SRC_DIR}/devops/build/automation/cloudberry/scripts/create-cloudberry-demo-cluster.sh"; then + echo "::error::Demo cluster creation failed" + exit 1 + fi + # pg_query_state requires the module in shared_preload_libraries. + su - gpadmin -c "cd ${SRC_DIR} && \ + source /usr/local/cloudberry-db/cloudberry-env.sh && \ + source gpAux/gpdemo/gpdemo-env.sh && \ + gpconfig -c shared_preload_libraries -v 'gp_stats_collector' && \ + gpstop -ra" + + - name: Run gp_stats_collector regression suite + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + chmod +x "${SRC_DIR}"/devops/build/automation/cloudberry/scripts/test-cloudberry.sh + # Capture make output to an artifact file: the raw job log gets + # truncated by the huge Build step, so tail it here on failure. + if ! time su - gpadmin -c "cd ${SRC_DIR} && \ + source /usr/local/cloudberry-db/cloudberry-env.sh && \ + source gpAux/gpdemo/gpdemo-env.sh && \ + SRC_DIR=${SRC_DIR} \ + PGOPTIONS='' \ + MAKE_NAME='GPSC Regress' \ + MAKE_TARGET=installcheck \ + MAKE_DIRECTORY=--directory=${SRC_DIR}/gpcontrib/gp_stats_collector \ + ${SRC_DIR}/devops/build/automation/cloudberry/scripts/test-cloudberry.sh \ + > ${SRC_DIR}/build-logs/gpsc-regress-make.log 2>&1"; then + echo "::error::gp_stats_collector installcheck failed" + echo "===== gpsc-regress-make.log (tail) =====" + tail -120 ${SRC_DIR}/build-logs/gpsc-regress-make.log 2>/dev/null || true + echo "===== regression.diffs =====" + cat ${SRC_DIR}/gpcontrib/gp_stats_collector/regression.diffs 2>/dev/null || true + exit 1 + fi + + - name: Run pg_query_state suite + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + if ! time su - gpadmin -c "cd ${SRC_DIR} && \ + source /usr/local/cloudberry-db/cloudberry-env.sh && \ + source gpAux/gpdemo/gpdemo-env.sh && \ + SRC_DIR=${SRC_DIR} \ + PGOPTIONS='' \ + MAKE_NAME='GPSC pg_query_state' \ + MAKE_TARGET=installcheck \ + MAKE_DIRECTORY=--directory=${SRC_DIR}/gpcontrib/gp_stats_collector/test \ + ${SRC_DIR}/devops/build/automation/cloudberry/scripts/test-cloudberry.sh \ + > ${SRC_DIR}/build-logs/gpsc-pqs-make.log 2>&1"; then + echo "::error::pg_query_state suite failed" + echo "===== gpsc-pqs-make.log (tail) =====" + tail -120 ${SRC_DIR}/build-logs/gpsc-pqs-make.log 2>/dev/null || true + echo "===== test/regression.diffs =====" + cat ${SRC_DIR}/gpcontrib/gp_stats_collector/test/regression.diffs 2>/dev/null || true + exit 1 + fi + + - name: Upload regression artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: gpsc-results-${{ matrix.os }} + path: | + cloudberry/gpcontrib/gp_stats_collector/regression.out + cloudberry/gpcontrib/gp_stats_collector/regression.diffs + cloudberry/gpcontrib/gp_stats_collector/results/ + cloudberry/gpcontrib/gp_stats_collector/test/results/ + cloudberry/gpcontrib/gp_stats_collector/test/regression.diffs + cloudberry/build-logs/ + retention-days: 7 + + # Runs the pg_query_state isolation2 suite (multi-session / happy-path checks + # that plain pg_regress cannot express). The fault injector (gp_inject_fault) + # is enabled by default (--enable-faultinjector=yes), so no debug build is + # needed. + test-gpsc-isolation2: + name: pg_query_state multi-session (gp_stats_collector) + runs-on: ubuntu-latest + container: + image: apache/incubator-cloudberry:cbdb-build-ubuntu22.04-latest + options: >- + --user root + -h cdw + + steps: + - name: Checkout Cloudberry source + uses: actions/checkout@v4 + with: + path: cloudberry + submodules: recursive + + - name: Cloudberry Environment Initialization + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + if ! su - gpadmin -c "/tmp/init_system.sh"; then + echo "::error::Container initialization failed" + exit 1 + fi + mkdir -p "${SRC_DIR}/build-logs" + chown -R gpadmin:gpadmin . + chmod -R 755 . + + - name: Configure + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + chmod +x "${SRC_DIR}"/devops/build/automation/cloudberry/scripts/configure-cloudberry.sh + if ! time su - gpadmin -c "cd ${SRC_DIR} && SRC_DIR=${SRC_DIR} ${SRC_DIR}/devops/build/automation/cloudberry/scripts/configure-cloudberry.sh"; then + echo "::error::Configure failed" + exit 1 + fi + + - name: Build + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + chmod +x "${SRC_DIR}"/devops/build/automation/cloudberry/scripts/build-cloudberry.sh + if ! time su - gpadmin -c "cd ${SRC_DIR} && SRC_DIR=${SRC_DIR} ${SRC_DIR}/devops/build/automation/cloudberry/scripts/build-cloudberry.sh"; then + echo "::error::Build failed" + exit 1 + fi + + - name: Build isolation2 harness + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + if ! time su - gpadmin -c "cd ${SRC_DIR} && \ + source /usr/local/cloudberry-db/cloudberry-env.sh && \ + make -C src/test/isolation2 install"; then + echo "::error::isolation2 harness build failed" + exit 1 + fi + + - name: Create demo cluster (gp_stats_collector preloaded) + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + chmod +x "${SRC_DIR}"/devops/build/automation/cloudberry/scripts/create-cloudberry-demo-cluster.sh + if ! time su - gpadmin -c "cd ${SRC_DIR} && SRC_DIR=${SRC_DIR} ${SRC_DIR}/devops/build/automation/cloudberry/scripts/create-cloudberry-demo-cluster.sh"; then + echo "::error::Demo cluster creation failed" + exit 1 + fi + su - gpadmin -c "cd ${SRC_DIR} && \ + source /usr/local/cloudberry-db/cloudberry-env.sh && \ + source gpAux/gpdemo/gpdemo-env.sh && \ + gpconfig -c shared_preload_libraries -v 'gp_stats_collector' && \ + gpstop -ra" + + - name: Run pg_query_state isolation2 suite + shell: bash + env: + SRC_DIR: ${{ github.workspace }}/cloudberry + run: | + set -eo pipefail + if ! su - gpadmin -c "cd ${SRC_DIR} && \ + source /usr/local/cloudberry-db/cloudberry-env.sh && \ + source gpAux/gpdemo/gpdemo-env.sh && \ + make -C gpcontrib/gp_stats_collector/test/isolation2 installcheck \ + > ${SRC_DIR}/build-logs/gpsc-iso2.log 2>&1"; then + echo "::error::pg_query_state isolation2 suite failed" + echo "===== gpsc-iso2.log (tail) =====" + tail -100 ${SRC_DIR}/build-logs/gpsc-iso2.log 2>/dev/null || true + echo "===== isolation2 regression.diffs =====" + cat ${SRC_DIR}/gpcontrib/gp_stats_collector/test/isolation2/regression.diffs 2>/dev/null || true + exit 1 + fi + + - name: Upload isolation2 artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: gpsc-iso2-results + path: | + cloudberry/gpcontrib/gp_stats_collector/test/isolation2/results/ + cloudberry/gpcontrib/gp_stats_collector/test/isolation2/regression.diffs + cloudberry/build-logs/ + retention-days: 7 diff --git a/LICENSE b/LICENSE index 0ccd7072122..7aaffa76495 100644 --- a/LICENSE +++ b/LICENSE @@ -200,6 +200,28 @@ See the License for the specific language governing permissions and limitations under the License. +================================================================================ +This product includes software derived from pg_query_state +(https://github.com/postgrespro/pg_query_state), under the PostgreSQL License: + + Copyright (c) 2016-2025, Postgres Professional + + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose, without fee, and without a written agreement + is hereby granted, provided that the above copyright notice and this + paragraph and the following two paragraphs appear in all copies. + + IN NO EVENT SHALL POSTGRES PROFESSIONAL BE LIABLE TO ANY PARTY FOR DIRECT, + INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST + PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN + IF POSTGRES PROFESSIONAL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + POSTGRES PROFESSIONAL SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, + AND POSTGRES PROFESSIONAL HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, + UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + ================================================================================ This product includes software from PostgreSQL, under the PostgreSQL License: diff --git a/gpcontrib/gp_stats_collector/Makefile b/gpcontrib/gp_stats_collector/Makefile index b3228d2c45e..7c8e2b269af 100644 --- a/gpcontrib/gp_stats_collector/Makefile +++ b/gpcontrib/gp_stats_collector/Makefile @@ -3,7 +3,7 @@ EXTENSION = gp_stats_collector DATA = $(wildcard *--*.sql) REGRESS = gpsc_cursors gpsc_dist gpsc_select gpsc_utf8_trim gpsc_utility gpsc_guc_cache gpsc_uds gpsc_locale -PROTO_BASES = gpsc_plan gpsc_metrics gpsc_set_service +PROTO_BASES = gpsc_plan gpsc_metrics gpsc_set_service yagpcc_metrics yagpcc_plan yagpcc_set_per_node PROTO_OBJS = $(patsubst %,src/protos/%.pb.o,$(PROTO_BASES)) C_OBJS = $(patsubst %.c,%.o,$(wildcard src/*.c src/*/*.c)) @@ -11,6 +11,7 @@ CPP_OBJS = $(patsubst %.cpp,%.o,$(wildcard src/*.cpp src/log/*.cpp src/memory/*. OBJS = $(C_OBJS) $(CPP_OBJS) $(PROTO_OBJS) PG_CXXFLAGS += -Werror -Wall -Wno-unused-but-set-variable -std=c++17 -Isrc/protos -Isrc -Iinclude -DGPBUILD +PG_CPPFLAGS += -I$(libpq_srcdir) -Isrc/protos -Isrc -Iinclude SHLIB_LINK += -lprotobuf -lstdc++ EXTRA_CLEAN = src/protos diff --git a/gpcontrib/gp_stats_collector/README.md b/gpcontrib/gp_stats_collector/README.md index 8c2d5c6868e..8fb5ba2ce84 100644 --- a/gpcontrib/gp_stats_collector/README.md +++ b/gpcontrib/gp_stats_collector/README.md @@ -45,3 +45,25 @@ An extension for collecting query execution metrics and reporting them to an ext - **User Filtering:** To exclude activity from certain roles, add them to the comma-separated list in `gpsc.ignored_users_list`. - **Trimming plans:** Query texts and execution plans are trimmed based on `gpsc.max_text_size` and `gpsc.max_plan_size` (default: 1024KB). For now, it is not recommended to set these GUCs higher than 1024KB. - **Analyze collection:** Analyze is sent if execution time exceeds `gpsc.min_analyze_time`, which is 10 seconds by default. Analyze is collected if `gpsc.enable_analyze` is true. + +### Runtime Query State (`pg_query_state`) + +On-demand inspection of the live execution state of another running backend. The target's active plan tree is walked across the coordinator (QD) and every segment (QE), collecting per-node instrumentation, without waiting for the query to finish. This is the signal-only variant: per-node samples are written to the server log rather than sent to the UDS sink. + +The functions live in the `gpsc` schema (extension version 1.2). + +#### 1. `pg_query_state(pid)` +- **What:** Triggers runtime per-node collection for the query running on backend `pid`. Fans a poll out to every participating QE and to the QD; each backend walks its plan tree and logs a per-node snapshot. Fire-and-forget: returns `void`. +- **GUC:** `pg_query_state.enable`. + +#### 2. `pg_query_state_backends(pid)` +- **What:** Lists the QE backends participating in the query running on backend `pid`, as `(segid, pid)` rows. Returns an empty set when the target is not running a query or has the module disabled. +- **GUC:** `pg_query_state.enable`. + +#### 3. `cbdb_mpp_query_state(gp_segment_pid[])` +- **What:** QE-side dispatch target used internally by `pg_query_state()`; not intended for direct use. + +### Runtime Query State Configuration +- **Enable:** `pg_query_state.enable` (default `on`) turns the executor hooks and signal handling on or off. Additional GUCs `pg_query_state.enable_timing` and `pg_query_state.enable_buffers` control the level of instrumentation collected. +- **Permissions:** The functions are granted to `PUBLIC`, but access is checked in the server: a caller may poll a backend only if it is a superuser or owns the target query. This lets monitoring agents run under a non-superuser role while still preventing one role from observing another's queries. +- **Preload:** The module registers custom signal handlers at startup, so `gp_stats_collector` must be listed in `shared_preload_libraries`. diff --git a/gpcontrib/gp_stats_collector/gp_stats_collector--1.1--1.2.sql b/gpcontrib/gp_stats_collector/gp_stats_collector--1.1--1.2.sql new file mode 100644 index 00000000000..ce0659d5032 --- /dev/null +++ b/gpcontrib/gp_stats_collector/gp_stats_collector--1.1--1.2.sql @@ -0,0 +1,49 @@ +/* gp_stats_collector--1.1--1.2.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION gp_stats_collector UPDATE TO '1.2'" to load this file. \quit + +-- Compact (segid, pid) identifier for a QE backend running on a segment. +-- Matches the C gp_segment_pid struct used by the pg_query_state signal layer. +CREATE TYPE gpsc.gp_segment_pid AS ( + segid int, + pid int +); + +-- pg_query_state(pid): trigger runtime per-node collection for the query +-- running on backend `pid`. Fans QueryStatePollReason out to every QE via +-- cbdb_mpp_query_state; each matching QE walks its plan tree and pushes a +-- per-node batch to its local yagpcc over UDS. Fire-and-forget: returns void. +CREATE FUNCTION gpsc.pg_query_state(pid int, trace_id bytea) +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'pg_query_state' +LANGUAGE C VOLATILE EXECUTE ON COORDINATOR; + +-- cbdb_mpp_query_state(gp_segment_pid[], trace_id): dispatched verbatim to +-- every segment by pg_query_state() via CdbDispatchCommand; runs locally on +-- each QE, so no EXECUTE ON marker. Signals the matching local backends. The +-- trace_id is stamped into every per-node batch so all backends' pushes land +-- under the one key this collection owns. +CREATE FUNCTION gpsc.cbdb_mpp_query_state(gpsc.gp_segment_pid[], trace_id bytea) +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'cbdb_mpp_query_state' +LANGUAGE C VOLATILE; + +-- pg_query_state_backends(pid): list the QE backends participating in the +-- query running on backend `pid`, as (segid, pid) rows. yagpcc uses the row +-- count as the "expected batches" barrier: per-node collection is complete +-- once a batch has arrived from every listed backend. +CREATE FUNCTION gpsc.pg_query_state_backends(pid int) +RETURNS TABLE(segid int, pid int) +AS 'MODULE_PATHNAME', 'pg_query_state_backends' +LANGUAGE C VOLATILE EXECUTE ON COORDINATOR; + +-- The runtime query-state API is callable by any role; the per-backend +-- permission gate in C (superuser or the query's owner) enforces access, so +-- these can be granted broadly. This lets monitoring agents (e.g. yagpcc) run +-- under a non-superuser role. cbdb_mpp_query_state is dispatched to the QEs +-- under the caller's role, so it needs EXECUTE too. +GRANT USAGE ON SCHEMA gpsc TO PUBLIC; +GRANT EXECUTE ON FUNCTION gpsc.pg_query_state(int, bytea) TO PUBLIC; +GRANT EXECUTE ON FUNCTION gpsc.pg_query_state_backends(int) TO PUBLIC; +GRANT EXECUTE ON FUNCTION gpsc.cbdb_mpp_query_state(gpsc.gp_segment_pid[], bytea) TO PUBLIC; \ No newline at end of file diff --git a/gpcontrib/gp_stats_collector/gp_stats_collector--1.2.sql b/gpcontrib/gp_stats_collector/gp_stats_collector--1.2.sql new file mode 100644 index 00000000000..8e3bbeeae88 --- /dev/null +++ b/gpcontrib/gp_stats_collector/gp_stats_collector--1.2.sql @@ -0,0 +1,159 @@ +/* gp_stats_collector--1.2.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION gp_stats_collector" to load this file. \quit + +CREATE SCHEMA gpsc; + +CREATE FUNCTION gpsc.__stat_messages_reset_f_on_master() +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'gpsc_stat_messages_reset' +LANGUAGE C EXECUTE ON COORDINATOR; + +CREATE FUNCTION gpsc.__stat_messages_reset_f_on_segments() +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'gpsc_stat_messages_reset' +LANGUAGE C EXECUTE ON ALL SEGMENTS; + +CREATE FUNCTION gpsc.stat_messages_reset() +RETURNS SETOF void +AS +$$ + SELECT gpsc.__stat_messages_reset_f_on_master(); + SELECT gpsc.__stat_messages_reset_f_on_segments(); +$$ +LANGUAGE SQL EXECUTE ON COORDINATOR; + +CREATE FUNCTION gpsc.__stat_messages_f_on_master() +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'gpsc_stat_messages' +LANGUAGE C STRICT VOLATILE EXECUTE ON COORDINATOR; + +CREATE FUNCTION gpsc.__stat_messages_f_on_segments() +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'gpsc_stat_messages' +LANGUAGE C STRICT VOLATILE EXECUTE ON ALL SEGMENTS; + +CREATE VIEW gpsc.stat_messages AS + SELECT C.* + FROM gpsc.__stat_messages_f_on_master() as C ( + segid int, + total_messages bigint, + send_failures bigint, + connection_failures bigint, + other_errors bigint, + max_message_size int + ) + UNION ALL + SELECT C.* + FROM gpsc.__stat_messages_f_on_segments() as C ( + segid int, + total_messages bigint, + send_failures bigint, + connection_failures bigint, + other_errors bigint, + max_message_size int + ) +ORDER BY segid; + +CREATE FUNCTION gpsc.__init_log_on_master() +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'gpsc_init_log' +LANGUAGE C STRICT VOLATILE EXECUTE ON COORDINATOR; + +CREATE FUNCTION gpsc.__init_log_on_segments() +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'gpsc_init_log' +LANGUAGE C STRICT VOLATILE EXECUTE ON ALL SEGMENTS; + +-- Creates log table inside gpsc schema. +SELECT gpsc.__init_log_on_master(); +SELECT gpsc.__init_log_on_segments(); + +CREATE VIEW gpsc.log AS + SELECT * FROM gpsc.__log -- master + UNION ALL + SELECT * FROM gp_dist_random('gpsc.__log') -- segments +ORDER BY tmid, ssid, ccnt; + +CREATE FUNCTION gpsc.__truncate_log_on_master() +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'gpsc_truncate_log' +LANGUAGE C STRICT VOLATILE EXECUTE ON COORDINATOR; + +CREATE FUNCTION gpsc.__truncate_log_on_segments() +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'gpsc_truncate_log' +LANGUAGE C STRICT VOLATILE EXECUTE ON ALL SEGMENTS; + +CREATE FUNCTION gpsc.truncate_log() +RETURNS SETOF void AS $$ +BEGIN + PERFORM gpsc.__truncate_log_on_master(); + PERFORM gpsc.__truncate_log_on_segments(); +END; +$$ LANGUAGE plpgsql VOLATILE; + +CREATE FUNCTION gpsc.__test_uds_start_server(path text) +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'gpsc_test_uds_start_server' +LANGUAGE C STRICT EXECUTE ON COORDINATOR; + +CREATE FUNCTION gpsc.__test_uds_receive(timeout_ms int DEFAULT 2000) +RETURNS SETOF bigint +AS 'MODULE_PATHNAME', 'gpsc_test_uds_receive' +LANGUAGE C STRICT EXECUTE ON COORDINATOR; + +CREATE FUNCTION gpsc.__test_uds_stop_server() +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'gpsc_test_uds_stop_server' +LANGUAGE C EXECUTE ON COORDINATOR; + +-- --------------------------------------------------------------------------- +-- 1.2: pg_query_state per-node runtime collection (push to yagpcc via UDS) +-- --------------------------------------------------------------------------- + +-- Compact (segid, pid) identifier for a QE backend running on a segment. +-- Matches the C gp_segment_pid struct used by the pg_query_state signal layer. +CREATE TYPE gpsc.gp_segment_pid AS ( + segid int, + pid int +); + +-- pg_query_state(pid): trigger runtime per-node collection for the query +-- running on backend `pid`. Fans QueryStatePollReason out to every QE via +-- cbdb_mpp_query_state; each matching QE walks its plan tree and pushes a +-- per-node batch to its local yagpcc over UDS. Fire-and-forget: returns void. +CREATE FUNCTION gpsc.pg_query_state(pid int, trace_id bytea) +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'pg_query_state' +LANGUAGE C VOLATILE EXECUTE ON COORDINATOR; + +-- cbdb_mpp_query_state(gp_segment_pid[], trace_id): dispatched verbatim to +-- every segment by pg_query_state() via CdbDispatchCommand; runs locally on +-- each QE, so no EXECUTE ON marker. Signals the matching local backends. The +-- trace_id is stamped into every per-node batch so all backends' pushes land +-- under the one key this collection owns. +CREATE FUNCTION gpsc.cbdb_mpp_query_state(gpsc.gp_segment_pid[], trace_id bytea) +RETURNS SETOF void +AS 'MODULE_PATHNAME', 'cbdb_mpp_query_state' +LANGUAGE C VOLATILE; + +-- pg_query_state_backends(pid): list the QE backends participating in the +-- query running on backend `pid`, as (segid, pid) rows. yagpcc uses the row +-- count as the "expected batches" barrier: per-node collection is complete +-- once a batch has arrived from every listed backend. +CREATE FUNCTION gpsc.pg_query_state_backends(pid int) +RETURNS TABLE(segid int, pid int) +AS 'MODULE_PATHNAME', 'pg_query_state_backends' +LANGUAGE C VOLATILE EXECUTE ON COORDINATOR; + +-- The runtime query-state API is callable by any role; the per-backend +-- permission gate in C (superuser or the query's owner) enforces access, so +-- these can be granted broadly. This lets monitoring agents (e.g. yagpcc) run +-- under a non-superuser role. cbdb_mpp_query_state is dispatched to the QEs +-- under the caller's role, so it needs EXECUTE too. +GRANT USAGE ON SCHEMA gpsc TO PUBLIC; +GRANT EXECUTE ON FUNCTION gpsc.pg_query_state(int, bytea) TO PUBLIC; +GRANT EXECUTE ON FUNCTION gpsc.pg_query_state_backends(int) TO PUBLIC; +GRANT EXECUTE ON FUNCTION gpsc.cbdb_mpp_query_state(gpsc.gp_segment_pid[], bytea) TO PUBLIC; \ No newline at end of file diff --git a/gpcontrib/gp_stats_collector/gp_stats_collector.control b/gpcontrib/gp_stats_collector/gp_stats_collector.control index 4aea2bd49b8..76cf6c26e2b 100644 --- a/gpcontrib/gp_stats_collector/gp_stats_collector.control +++ b/gpcontrib/gp_stats_collector/gp_stats_collector.control @@ -1,5 +1,5 @@ # gp_stats_collector extension comment = 'Intercept query and plan execution hooks and report them to Cloudberry monitor agents' -default_version = '1.1' +default_version = '1.2' module_pathname = '$libdir/gp_stats_collector' superuser = true diff --git a/gpcontrib/gp_stats_collector/protos/yagpcc_metrics.proto b/gpcontrib/gp_stats_collector/protos/yagpcc_metrics.proto new file mode 100644 index 00000000000..5703aeb1f60 --- /dev/null +++ b/gpcontrib/gp_stats_collector/protos/yagpcc_metrics.proto @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package yagpcc; + +/* + * Instrumentation counters for a single plan node execution. + * + * Field semantics mirror the PostgreSQL Instrumentation struct: + * ntuples -- total tuples produced (completed loops) + * nloops -- number of completed execution loops + * tuplecount -- tuples emitted so far in the current (in-progress) loop + * firsttuple -- wall time to first tuple of this cycle (seconds) + * startup -- total startup time across all loops (seconds) + * total -- total elapsed time across all loops (seconds) + */ +message MetricInstrumentation { + uint64 ntuples = 1; + uint64 nloops = 2; + uint64 tuplecount = 3; + double firsttuple = 4; + double startup = 5; + double total = 6; + uint64 shared_blks_hit = 7; + uint64 shared_blks_read = 8; +} + +/* + * Node-level metrics container. Currently wraps only instrumentation; may + * be extended with system/spill stats in future revisions. + */ +message NodeMetrics { + MetricInstrumentation instrumentation = 1; +} diff --git a/gpcontrib/gp_stats_collector/protos/yagpcc_plan.proto b/gpcontrib/gp_stats_collector/protos/yagpcc_plan.proto new file mode 100644 index 00000000000..088b1aa8346 --- /dev/null +++ b/gpcontrib/gp_stats_collector/protos/yagpcc_plan.proto @@ -0,0 +1,79 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +package yagpcc; + +message SetQueryPlanReq { + google.protobuf.Timestamp datetime = 1; + QueryKey query_key = 2; + string plan_doc = 3; /* ExplainPrintPlan output */ + int32 format = 4; /* ExplainFormat: 0=text 1=xml 2=json 3=yaml */ +} + +/* + * Execution status of a single plan node. + * + * Mirrors QsNodeStatus from qs_types.h: + * INITIALIZED -- node was set up but has not yet started execution + * EXECUTING -- node is currently inside a tuple-fetch call + * FINISHED -- node has completed at least one full execution loop + */ +enum PlanNodeStatus { + PLAN_NODE_STATUS_UNSPECIFIED = 0; + PLAN_NODE_STATUS_INITIALIZED = 1; + PLAN_NODE_STATUS_EXECUTING = 2; + PLAN_NODE_STATUS_FINISHED = 3; +} + +/* + * Identifying information for a single plan node within a query plan tree. + * + * Fields: + * plan_node_id -- unique node id within the plan (Plan.plan_node_id) + * parent_plan_node_id -- plan_node_id of the logical parent node, or 0 + * node_type -- PostgreSQL NodeTag value (nodeTag(plan)) + * slice_id -- CDB slice index (currentSliceId) + * plan_rows -- optimizer row-count estimate (Plan.plan_rows) + * relation_oid -- OID of the scanned relation for scan nodes, or 0 + */ +message PlanNode { + int32 plan_node_id = 1; + int32 parent_plan_node_id = 2; + int32 node_type = 3; + int32 slice_id = 4; + double plan_rows = 5; + int32 relation_oid = 6; +} + +/* + * Common query and segment identification keys reused across messages. + */ +message QueryKey { + int32 tmid = 1; /* gp_gettmid() transaction/time identifier */ + int32 ssid = 2; /* gp_session_id */ + int32 ccnt = 3; /* gp_command_count */ +} + +message SegmentKey { + int32 dbid = 1; /* GpIdentity.dbid */ + int32 segindex = 2; /* GpIdentity.segindex (-1 = coordinator) */ +} diff --git a/gpcontrib/gp_stats_collector/protos/yagpcc_set_per_node.proto b/gpcontrib/gp_stats_collector/protos/yagpcc_set_per_node.proto new file mode 100644 index 00000000000..2323f72bd2a --- /dev/null +++ b/gpcontrib/gp_stats_collector/protos/yagpcc_set_per_node.proto @@ -0,0 +1,83 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +import "protos/yagpcc_plan.proto"; + +package yagpcc; + +/* + * SetPerNodeBatchReq -- one whole plan-tree snapshot from a single backend. + * + * Sent once per walker pass (SendQueryState signal or pg_qs_executor_end), + * carrying every observed plan node in one message. A backend opens one UDS + * connection and sends this batch instead of connect+send+close per node. The + * shared query_key, segment_key and datetime are hoisted out of every node. + * + * Wire transport: UDSConnector::report_per_node_batch() with the 8-byte + * extended protocol header (payload_size | 0x80000000, request_type=1, + * reserved=0). + * + * This message MUST stay byte-identical to the yagpcc-side definition in + * api/proto/agent_segment/yagpcc_set_service.proto. + */ +message SetPerNodeBatchReq { + google.protobuf.Timestamp datetime = 1; + SegmentKey segment_key = 2; + repeated BatchNode nodes = 3; + bytes trace_id = 4; +} + +/* + * BatchNode -- one plan node inside a SetPerNodeBatchReq. + * + * Deliberately flat (no NodeMetrics wrapper) so the two repos can keep the + * message trivially wire-identical without sharing a metrics wrapper type. + * Fields mirror GpscNodeSample minus the hoisted identity keys. + */ +message BatchNode { + int32 plan_node_id = 1; + int32 parent_plan_node_id = 2; + int32 node_type = 3; /* raw NodeTag value */ + int32 slice_id = 4; + double plan_rows = 5; /* planner estimate */ + int32 relation_oid = 6; /* scan relation OID, 0 otherwise */ + double ntuples = 7; + double tuplecount = 8; + double nloops = 9; + double startup = 10; + double total = 11; + double firsttuple = 12; + uint64 shared_blks_hit = 13; + uint64 shared_blks_read = 14; + PlanNodeStatus node_status = 15; + bool eof = 16; + google.protobuf.Timestamp executed_at = 17; + bool workfile_created = 18; + int64 workmem_used = 19; /* bytes of work_mem actually used */ + int64 workmem_wanted = 20; /* bytes needed to avoid spill; >0 == spilled */ + double ntuples_delta = 21; /* tuples produced since the previous sample */ + double tuples_per_sec = 22; /* ntuples_delta over the sample interval */ + double time_since_init_sec = 23; /* seconds since the node's first sample */ + bool stalled = 24; /* executing, no new tuples, not at eof */ + int32 pid = 25; +} + diff --git a/gpcontrib/gp_stats_collector/src/GpscStat.cpp b/gpcontrib/gp_stats_collector/src/GpscStat.cpp index 151cfd87c02..7e326e6b44a 100644 --- a/gpcontrib/gp_stats_collector/src/GpscStat.cpp +++ b/gpcontrib/gp_stats_collector/src/GpscStat.cpp @@ -40,19 +40,57 @@ extern "C" { namespace { + +/* + * ProtectedData -- spin-lock-protected wrapper around the GpscStat counters. + * + * Lives in a shared-memory segment so all backends on the same segment host + * contribute to the same counters. + */ struct ProtectedData { - slock_t mutex; + slock_t mutex; GpscStat::Data data; }; -shmem_startup_hook_type prev_shmem_startup_hook = NULL; -ProtectedData *data = nullptr; -void +static shmem_startup_hook_type prev_shmem_startup_hook = NULL; + +/* + * prev_shmem_request_hook is only relevant on PostgreSQL 15+, where the + * shmem request phase is separate from the startup phase. + */ +#if PG_VERSION_NUM >= 150000 +static shmem_request_hook_type prev_shmem_request_hook = NULL; + +/* + * gpsc_shmem_request -- request shared memory space. + * + * Installed as shmem_request_hook on PG15+. Chains to the previous hook + * before adding our own request. + */ +static void +gpsc_shmem_request() +{ + if (prev_shmem_request_hook) + prev_shmem_request_hook(); + RequestAddinShmemSpace(sizeof(ProtectedData)); +} +#endif /* PG_VERSION_NUM >= 150000 */ + +static ProtectedData *data = nullptr; + +/* + * gpsc_shmem_startup -- attach to (or initialise) the GpscStat shared segment. + * + * Installed as shmem_startup_hook. On first call (found == false) zeroes the + * counters and initialises the spin lock. Always chains to the previous hook. + */ +static void gpsc_shmem_startup() { if (prev_shmem_startup_hook) prev_shmem_startup_hook(); + LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); bool found; data = reinterpret_cast( @@ -65,10 +103,16 @@ gpsc_shmem_startup() LWLockRelease(AddinShmemInitLock); } +/* + * LockGuard -- RAII wrapper around a SpinLock. + * + * Acquires the spin lock on construction and releases it on destruction, + * ensuring the lock is always released even if an exception is thrown. + */ class LockGuard { public: - LockGuard(slock_t *mutex) : mutex_(mutex) + explicit LockGuard(slock_t *mutex) : mutex_(mutex) { SpinLockAcquire(mutex_); } @@ -80,24 +124,51 @@ class LockGuard private: slock_t *mutex_; }; + } // namespace +/* + * GpscStat::init -- install shmem hooks during shared_preload_libraries phase. + * + * Must be called while process_shared_preload_libraries_in_progress is true. + * On PostgreSQL 14 and earlier, shared memory is requested here directly via + * RequestAddinShmemSpace(). On PostgreSQL 15+ a separate shmem_request_hook + * handles the request. + */ void GpscStat::init() { if (!process_shared_preload_libraries_in_progress) return; + +#if PG_VERSION_NUM >= 150000 + prev_shmem_request_hook = shmem_request_hook; + shmem_request_hook = gpsc_shmem_request; +#else RequestAddinShmemSpace(sizeof(ProtectedData)); +#endif + prev_shmem_startup_hook = shmem_startup_hook; shmem_startup_hook = gpsc_shmem_startup; } +/* + * GpscStat::deinit -- restore shmem hooks to their previous values. + * + * Called from hooks_deinit(). + */ void GpscStat::deinit() { +#if PG_VERSION_NUM >= 150000 + shmem_request_hook = prev_shmem_request_hook; +#endif shmem_startup_hook = prev_shmem_startup_hook; } +/* + * GpscStat::reset -- zero all counters in the shared segment. + */ void GpscStat::reset() { @@ -105,6 +176,12 @@ GpscStat::reset() data->data = GpscStat::Data(); } +/* + * GpscStat::report_send -- record a successful message send. + * + * Parameters: + * msg_size -- size of the sent protobuf message in bytes + */ void GpscStat::report_send(int32_t msg_size) { @@ -114,6 +191,9 @@ GpscStat::report_send(int32_t msg_size) std::max(msg_size, data->data.max_message_size); } +/* + * GpscStat::report_bad_connection -- record a failed UDS connection attempt. + */ void GpscStat::report_bad_connection() { @@ -122,6 +202,12 @@ GpscStat::report_bad_connection() data->data.failed_connects++; } +/* + * GpscStat::report_bad_send -- record a failed send on an established connection. + * + * Parameters: + * msg_size -- size of the message that could not be sent + */ void GpscStat::report_bad_send(int32_t msg_size) { @@ -132,6 +218,9 @@ GpscStat::report_bad_send(int32_t msg_size) std::max(msg_size, data->data.max_message_size); } +/* + * GpscStat::report_error -- record any other error not covered by the above. + */ void GpscStat::report_error() { @@ -140,6 +229,12 @@ GpscStat::report_error() data->data.failed_other++; } +/* + * GpscStat::get_stats -- return a snapshot of all counters. + * + * The snapshot is taken under the spin lock and returned by value, so the + * caller sees a consistent view. + */ GpscStat::Data GpscStat::get_stats() { @@ -147,6 +242,12 @@ GpscStat::get_stats() return data->data; } +/* + * GpscStat::loaded -- return true when the shared segment has been mapped. + * + * Returns false before shmem_startup_hook has run (e.g. if the extension was + * not loaded via shared_preload_libraries). + */ bool GpscStat::loaded() { diff --git a/gpcontrib/gp_stats_collector/src/PlanNodeEmitter.cpp b/gpcontrib/gp_stats_collector/src/PlanNodeEmitter.cpp new file mode 100644 index 00000000000..934e12b3149 --- /dev/null +++ b/gpcontrib/gp_stats_collector/src/PlanNodeEmitter.cpp @@ -0,0 +1,174 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * PlanNodeEmitter.cpp + * + * IDENTIFICATION + * gpcontrib/gp_stats_collector/src/PlanNodeEmitter.cpp + * + *------------------------------------------------------------------------- + */ + +/* + * PlanNodeEmitter.cpp + * Build and send per-node protobuf messages to the yagpcc UDS sink. + * + * This file is the bridge between the C pg_query_state layer and the C++ + * protobuf / UDS connector infrastructure. It implements the functions + * declared in PlanNodeEmitter.h and callable from plain C: + * + * gpsc_qs_sync_config() -- reload the Config singleton + * gpsc_emit_node_batch() -- serialize a plan-tree snapshot and send it + * gpsc_emit_query_plan() -- serialize a plan document and send it + * + * The outgoing message types are yagpcc::SetPerNodeBatchReq and + * yagpcc::SetQueryPlanReq (generated from protos/yagpcc_set_per_node.proto). + * Transmission is handled by UDSConnector, which prepends the 8-byte extended + * protocol header before writing to the socket. + */ + +#include "PlanNodeEmitter.h" +#include "protos/yagpcc_set_per_node.pb.h" +#include "UDSConnector.h" +#include "Config.h" +#include "ProtoUtils.h" + +/* Module-private Config instance shared across all emit calls in a session. */ +static Config pne_config; + +/* + * gpsc_qs_sync_config -- reload the Config singleton. + * + * Must be called before a gpsc_emit_node_batch() call so that the UDS path + * and other settings are up to date. It is a no-op when the config has not + * changed since the last call. + */ +extern "C" void +gpsc_qs_sync_config() +{ + pne_config.sync(); +} + +/* + * map_node_status -- convert a QsNodeStatus enum to yagpcc::PlanNodeStatus. + * + * Returns PLAN_NODE_STATUS_UNSPECIFIED for any value not recognised by the + * switch, which is safe because the receiver ignores unknown status codes. + */ +static yagpcc::PlanNodeStatus +map_node_status(QsNodeStatus status) +{ + switch (status) + { + case QS_NODE_STATUS_INITIALIZED: + return yagpcc::PLAN_NODE_STATUS_INITIALIZED; + case QS_NODE_STATUS_EXECUTING: + return yagpcc::PLAN_NODE_STATUS_EXECUTING; + case QS_NODE_STATUS_FINISHED: + return yagpcc::PLAN_NODE_STATUS_FINISHED; + default: + return yagpcc::PLAN_NODE_STATUS_UNSPECIFIED; + } +} + +extern "C" void +gpsc_emit_node_batch(GpscNodeSample **nodes, int count, const char *trace_id) +{ + if (count <= 0) + return; + + yagpcc::SetPerNodeBatchReq request; + + /* Timestamp */ + *request.mutable_datetime() = current_ts(); + request.set_trace_id(trace_id, GPSC_TRACE_ID_LEN); + + auto *sk = request.mutable_segment_key(); + sk->set_dbid(nodes[0]->dbid); + sk->set_segindex(nodes[0]->segindex); + + for (int i = 0; i < count; i++) + { + GpscNodeSample *node = nodes[i]; + yagpcc::BatchNode *bn = request.add_nodes(); + + bn->set_pid(node->pid); + bn->set_plan_node_id(node->plan_node_id); + bn->set_parent_plan_node_id(node->parent_plan_node_id); + bn->set_node_type(node->node_tag); + bn->set_slice_id(node->slice_id); + bn->set_plan_rows(node->plan_rows); + bn->set_relation_oid(node->relation_oid); + bn->set_ntuples(node->ntuples); + bn->set_tuplecount(node->tuplecount); + bn->set_nloops(node->nloops); + bn->set_startup(node->startup); + bn->set_total(node->total); + bn->set_firsttuple(node->firsttuple); + bn->set_shared_blks_hit(node->shared_blks_hit); + bn->set_shared_blks_read(node->shared_blks_read); + bn->set_node_status(map_node_status(node->node_status)); + bn->set_eof(node->eof); + /* + * executed_at is the snapshot instant, shared by every node in this + * pass. It is stamped per node (not at message level) because the + * receiver aggregates nodes across segments and loses the batch + * grouping; each node needs its own compute time to derive a per-node + * rate. Same value as datetime here since one walk = one instant. + */ + *bn->mutable_executed_at() = request.datetime(); + bn->set_workfile_created(node->workfile_created); + bn->set_workmem_used(node->workmem_used); + bn->set_workmem_wanted(node->workmem_wanted); + /* + * Derived rate fields, computed in signal_handler from the per-node + * rolling state (prev ntuples + prev executed_at). They MUST be + * serialized here too: the receiver keys per invocation trace_id and + * sees each node once, so it cannot re-derive a rate on its side. + */ + bn->set_ntuples_delta(node->ntuples_delta); + bn->set_tuples_per_sec(node->tuples_per_sec); + bn->set_time_since_init_sec(node->time_since_init_sec); + bn->set_stalled(node->stalled); + } + + UDSConnector::report_per_node_batch(request, pne_config); +} + +extern "C" void +gpsc_emit_query_plan(int32_t tmid, int32_t ssid, int32_t ccnt, + const char *plan_doc, int32_t format) +{ + if (plan_doc == nullptr || plan_doc[0] == '\0') + return; + + yagpcc::SetQueryPlanReq request; + + *request.mutable_datetime() = current_ts(); + + auto *qk = request.mutable_query_key(); + qk->set_tmid(tmid); + qk->set_ssid(ssid); + qk->set_ccnt(ccnt); + + request.set_plan_doc(plan_doc); + request.set_format(format); + + UDSConnector::report_query_plan(request, pne_config); +} diff --git a/gpcontrib/gp_stats_collector/src/PlanNodeEmitter.h b/gpcontrib/gp_stats_collector/src/PlanNodeEmitter.h new file mode 100644 index 00000000000..b73dffcfb16 --- /dev/null +++ b/gpcontrib/gp_stats_collector/src/PlanNodeEmitter.h @@ -0,0 +1,47 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * PlanNodeEmitter.h + * + * IDENTIFICATION + * gpcontrib/gp_stats_collector/src/PlanNodeEmitter.h + * + *------------------------------------------------------------------------- + */ + +#ifndef PLAN_NODE_EMITTER_H +#define PLAN_NODE_EMITTER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "pg_query_state/qs_types.h" + +extern void gpsc_emit_node_batch(GpscNodeSample **nodes, int count, + const char *trace_id); +extern void gpsc_emit_query_plan(int32_t tmid, int32_t ssid, int32_t ccnt, + const char *plan_doc, int32_t format); +extern void gpsc_qs_sync_config(); + +#ifdef __cplusplus +} +#endif + +#endif /* PLAN_NODE_EMITTER_H */ diff --git a/gpcontrib/gp_stats_collector/src/UDSConnector.cpp b/gpcontrib/gp_stats_collector/src/UDSConnector.cpp index 056fa9071a5..51ce41f3548 100644 --- a/gpcontrib/gp_stats_collector/src/UDSConnector.cpp +++ b/gpcontrib/gp_stats_collector/src/UDSConnector.cpp @@ -31,6 +31,9 @@ #include "log/LogOps.h" #include "memory/gpdbwrappers.h" +#include +#include +#include #include #include #include @@ -42,99 +45,394 @@ extern "C" { #include "postgres.h" } -static void inline log_tracing_failure(const gpsc::SetQueryReq &req, - const std::string &event) +/* + * Extended protocol constants for the 8-byte header messages. + * + * kExtendedProtocolFlag is ORed into the 32-bit payload size field to signal + * to the receiver that this is an extended (8-byte) header rather than the + * original 4-byte header. + * + * The request-type word then selects the payload message: + * kRequestTypePerNodeBatch -> yagpcc::SetPerNodeBatchReq, + * kRequestTypeQueryPlan -> yagpcc::SetQueryPlanReq. + */ +static const uint32_t kExtendedProtocolFlag = 0x80000000u; +static const uint16_t kRequestTypePerNodeBatch = 1; +static const uint16_t kRequestTypeQueryPlan = 2; + +/* + * Bound on how long open_nonblocking_uds waits for a non-blocking connect() to + * finish when it returns EINPROGRESS/EAGAIN. Kept short: a local UDS handshake + * completes almost immediately, and we would rather drop a sample than stall the + * backend during a connection burst. + */ +static const int kConnectTimeoutMs = 50; + +/* + * Overall bound on how long send_all keeps retrying a message when the UDS send + * buffer stays full (EAGAIN). Backpressure is normal, but we must not block the + * backend indefinitely behind a stuck reader -- past this deadline we give up + * and drop the message. + */ +static const int kSendTimeoutMs = 200; + +/* ---------------------------------------------------------------- + * Error-logging helpers + * ---------------------------------------------------------------- */ + +/* + * log_query_failure -- emit a LOG message for a failed SetQueryReq send. + * + * Includes the query key (tmid/ssid/ccnt) and the triggering event name. + */ +static void inline +log_query_failure(const gpsc::SetQueryReq &req, const std::string &event) { ereport(LOG, (errmsg("Query {%d-%d-%d} %s tracing failed with error %m", req.query_key().tmid(), req.query_key().ssid(), req.query_key().ccnt(), event.c_str()))); } -bool -UDSConnector::report_query(const gpsc::SetQueryReq &req, - const std::string &event, const Config &config) +/* + * log_per_node_batch_failure -- emit a LOG message for a failed + * SetPerNodeBatchReq send. Includes the hex trace_id and node count. + */ +static void inline +log_per_node_batch_failure(const yagpcc::SetPerNodeBatchReq &req) { - sockaddr_un address{}; - address.sun_family = AF_UNIX; - const auto &uds_path = config.uds_path(); + static const char hexchars[] = "0123456789abcdef"; + const std::string &tid = req.trace_id(); + std::string hex; + + hex.reserve(tid.size() * 2); + for (unsigned char c : tid) + { + hex.push_back(hexchars[c >> 4]); + hex.push_back(hexchars[c & 0x0f]); + } + + ereport(LOG, (errmsg("Per-node batch {trace_id=%s} tracing (%d nodes) failed with error %m", + hex.c_str(), req.nodes_size()))); +} + +/* + * log_query_plan_failure -- emit a LOG message for a failed SetQueryPlanReq + * send. Includes the query key and plan-doc byte size. + */ +static void inline +log_query_plan_failure(const yagpcc::SetQueryPlanReq &req) +{ + ereport(LOG, (errmsg("Query {%d-%d-%d} plan-doc tracing (%zu bytes) failed with error %m", + req.query_key().tmid(), req.query_key().ssid(), + req.query_key().ccnt(), req.plan_doc().size()))); +} + +/* ---------------------------------------------------------------- + * Socket helpers + * ---------------------------------------------------------------- */ +/* + * open_nonblocking_uds -- create and connect a non-blocking AF_UNIX socket. + * + * Fills `address` from `uds_path`, creates a SOCK_STREAM socket, sets + * O_NONBLOCK, and connects. Returns the file descriptor on success. + * On any failure, records the error in GpscStat, logs at WARNING/LOG level, + * and returns -1. + * + * Parameters: + * uds_path -- filesystem path of the listening Unix-domain socket + * address -- caller-supplied sockaddr_un to fill (must be zero-initialised) + */ +static int +open_nonblocking_uds(const std::string &uds_path, sockaddr_un &address) +{ if (uds_path.size() >= sizeof(address.sun_path)) { ereport(WARNING, (errmsg("UDS path is too long for socket buffer"))); GpscStat::report_error(); - return false; + return -1; } + address.sun_family = AF_UNIX; strcpy(address.sun_path, uds_path.c_str()); - const auto sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + const int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd == -1) { - log_tracing_failure(req, event); GpscStat::report_error(); - return false; + return -1; } - // Close socket automatically on error path. - struct SockGuard - { - int fd; - ~SockGuard() - { - close(fd); - } - } sock_guard{sockfd}; - if (fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) { - // That's a very important error that should never happen, so make it - // visible to an end-user and admins. ereport(WARNING, (errmsg("Unable to create non-blocking socket connection %m"))); GpscStat::report_error(); - return false; + close(sockfd); + return -1; } if (connect(sockfd, reinterpret_cast(&address), sizeof(address)) == -1) { - log_tracing_failure(req, event); - GpscStat::report_bad_connection(); - return false; + if (errno != EINPROGRESS && errno != EAGAIN) + { + GpscStat::report_bad_connection(); + close(sockfd); + return -1; + } + + pollfd pfd = {}; + pfd.fd = sockfd; + pfd.events = POLLOUT; + + const int rc = poll(&pfd, 1, kConnectTimeoutMs); + if (rc <= 0) + { + GpscStat::report_bad_connection(); + close(sockfd); + return -1; + } + + int so_error = 0; + socklen_t len = sizeof(so_error); + if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_error, &len) == -1 || + so_error != 0) + { + GpscStat::report_bad_connection(); + close(sockfd); + return -1; + } } - const auto data_size = req.ByteSizeLong(); - const auto total_size = data_size + sizeof(uint32_t); - auto *buf = static_cast(gpdb::palloc(total_size)); - // Free buf automatically on error path. - struct BufGuard + return sockfd; +} + +/* + * monotonic_ms -- current CLOCK_MONOTONIC reading in milliseconds. + * + * Used for send_all's retry deadline; monotonic so it is immune to wall-clock + * jumps (NTP steps, settimeofday). + */ +static int64_t +monotonic_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t) ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +/* + * send_all -- write all `total_size` bytes from `buf` over `sockfd`. + * + * The socket is non-blocking and send() uses MSG_DONTWAIT. A full send buffer + * reports EAGAIN/EWOULDBLOCK -- normal backpressure, not an error -- so on that + * we wait (bounded by kSendTimeoutMs) for the socket to become writable via + * poll(POLLOUT) and retry, rather than dropping the message. EINTR retries + * immediately. Returns true once every byte is sent, false on a hard error or + * once the deadline is exceeded (errno is preserved on the hard-error path). + */ +static bool +send_all(int sockfd, const uint8_t *buf, size_t total_size) +{ + size_t sent_total = 0; + const int64_t deadline = monotonic_ms() + kSendTimeoutMs; + + while (sent_total < total_size) { - void *p; - ~BufGuard() + const ssize_t sent = send(sockfd, buf + sent_total, + total_size - sent_total, MSG_DONTWAIT); + if (sent > 0) { - gpdb::pfree(p); + sent_total += (size_t) sent; + continue; } - } buf_guard{buf}; - *reinterpret_cast(buf) = data_size; + /* errno is only meaningful when send() actually failed (sent < 0). */ + if (sent < 0 && errno == EINTR) + continue; + if (sent < 0 && errno != EAGAIN && errno != EWOULDBLOCK) + return false; /* hard error */ + + /* Send buffer full (or a 0-byte return): wait, bounded, for drain. */ + const int64_t remaining = deadline - monotonic_ms(); + if (remaining <= 0) + return false; /* sustained backpressure: give up */ + + pollfd pfd = {}; + pfd.fd = sockfd; + pfd.events = POLLOUT; + + const int rc = poll(&pfd, 1, (int) remaining); + if (rc < 0 && errno == EINTR) + continue; + if (rc <= 0) + return false; /* poll error or timeout */ + } + + return true; +} + +/* ---------------------------------------------------------------- + * Public methods + * ---------------------------------------------------------------- */ + +/* + * UDSConnector::report_query -- send a SetQueryReq with a 4-byte length header. + * + * Wire format: + * bytes 0-3: payload_size (uint32 LE, no flags) + * bytes 4+: serialized gpsc::SetQueryReq + * + * Parameters: + * req -- populated request message + * event -- label used in error log messages + * config -- current connector config (UDS path, etc.) + * + * Returns true on success, false on any failure. + */ +bool +UDSConnector::report_query(const gpsc::SetQueryReq &req, + const std::string &event, const Config &config) +{ + sockaddr_un address{}; + const auto &uds_path = config.uds_path(); + + const int sockfd = open_nonblocking_uds(uds_path, address); + if (sockfd == -1) + { + log_query_failure(req, event); + return false; + } + + struct SockGuard { int fd; ~SockGuard() { close(fd); } } sock_guard{sockfd}; + + const auto data_size = req.ByteSizeLong(); + const auto total_size = data_size + sizeof(uint32_t); + auto *buf = static_cast(gpdb::palloc(total_size)); + struct BufGuard { void *p; ~BufGuard() { gpdb::pfree(p); } } buf_guard{buf}; + + *reinterpret_cast(buf) = static_cast(data_size); req.SerializeWithCachedSizesToArray(buf + sizeof(uint32_t)); - int64_t sent = 0, sent_total = 0; - do + if (!send_all(sockfd, buf, total_size)) { - sent = send(sockfd, buf + sent_total, total_size - sent_total, - MSG_DONTWAIT); - if (sent > 0) - sent_total += sent; - } while (sent > 0 && size_t(sent_total) != total_size && - // the line below is a small throttling hack: - // if a message does not fit a single packet, we take a nap - // before sending the next one. - // Otherwise, MSG_DONTWAIT send might overflow the UDS - (pg_usleep(1000), true)); - - if (sent < 0) + log_query_failure(req, event); + GpscStat::report_bad_send(total_size); + return false; + } + + GpscStat::report_send(total_size); + return true; +} + +/* + * UDSConnector::report_per_node_batch -- send a SetPerNodeBatchReq with the + * 8-byte extended protocol header and request_type = 1. + * + * One socket open/write/close carries the whole + * plan-tree snapshot for a backend. + * + * Parameters: + * req -- populated batch request message + * config -- current connector config (UDS path, etc.) + * + * Returns true on success, false on any failure. + */ +bool +UDSConnector::report_per_node_batch(const yagpcc::SetPerNodeBatchReq &req, + const Config &config) +{ + sockaddr_un address{}; + const auto &uds_path = config.uds_path(); + + const int sockfd = open_nonblocking_uds(uds_path, address); + if (sockfd == -1) + { + log_per_node_batch_failure(req); + return false; + } + + struct SockGuard { int fd; ~SockGuard() { close(fd); } } sock_guard{sockfd}; + + const auto data_size = req.ByteSizeLong(); + const auto header_size = sizeof(uint32_t) + sizeof(uint16_t) + sizeof(uint16_t); + const auto total_size = header_size + data_size; + auto *buf = static_cast(gpdb::palloc(total_size)); + struct BufGuard { void *p; ~BufGuard() { gpdb::pfree(p); } } buf_guard{buf}; + + /* Write the 8-byte extended header. */ + uint8_t *p = buf; + *reinterpret_cast(p) = + static_cast(data_size) | kExtendedProtocolFlag; + p += sizeof(uint32_t); + *reinterpret_cast(p) = kRequestTypePerNodeBatch; + p += sizeof(uint16_t); + *reinterpret_cast(p) = 0; /* reserved */ + p += sizeof(uint16_t); + + req.SerializeWithCachedSizesToArray(p); + + if (!send_all(sockfd, buf, total_size)) + { + log_per_node_batch_failure(req); + GpscStat::report_bad_send(total_size); + return false; + } + + GpscStat::report_send(total_size); + return true; +} + +/* + * UDSConnector::report_query_plan -- send a SetQueryPlanReq with the 8-byte + * extended protocol header and request_type = 2. + * + * Same framing as report_per_node_batch(); only the request_type byte and the + * message type differ. + * + * Parameters: + * req -- populated plan-doc request message + * config -- current connector config (UDS path, etc.) + * + * Returns true on success, false on any failure. + */ +bool +UDSConnector::report_query_plan(const yagpcc::SetQueryPlanReq &req, + const Config &config) +{ + sockaddr_un address{}; + const auto &uds_path = config.uds_path(); + + const int sockfd = open_nonblocking_uds(uds_path, address); + if (sockfd == -1) + { + log_query_plan_failure(req); + return false; + } + + struct SockGuard { int fd; ~SockGuard() { close(fd); } } sock_guard{sockfd}; + + const auto data_size = req.ByteSizeLong(); + const auto header_size = sizeof(uint32_t) + sizeof(uint16_t) + sizeof(uint16_t); + const auto total_size = header_size + data_size; + auto *buf = static_cast(gpdb::palloc(total_size)); + struct BufGuard { void *p; ~BufGuard() { gpdb::pfree(p); } } buf_guard{buf}; + + /* Write the 8-byte extended header. */ + uint8_t *p = buf; + *reinterpret_cast(p) = + static_cast(data_size) | kExtendedProtocolFlag; + p += sizeof(uint32_t); + *reinterpret_cast(p) = kRequestTypeQueryPlan; + p += sizeof(uint16_t); + *reinterpret_cast(p) = 0; /* reserved */ + p += sizeof(uint16_t); + + req.SerializeWithCachedSizesToArray(p); + + if (!send_all(sockfd, buf, total_size)) { - log_tracing_failure(req, event); + log_query_plan_failure(req); GpscStat::report_bad_send(total_size); return false; } diff --git a/gpcontrib/gp_stats_collector/src/UDSConnector.h b/gpcontrib/gp_stats_collector/src/UDSConnector.h index ac56dd54f44..ac62241cb11 100644 --- a/gpcontrib/gp_stats_collector/src/UDSConnector.h +++ b/gpcontrib/gp_stats_collector/src/UDSConnector.h @@ -29,14 +29,66 @@ #define UDSCONNECTOR_H #include "protos/gpsc_set_service.pb.h" +#include "protos/yagpcc_set_per_node.pb.h" class Config; +/* + * UDSConnector -- thin static helper that sends protobuf messages over a + * Unix-domain socket. + * + * All methods open a fresh non-blocking SOCK_STREAM connection, serialise + * the protobuf, write it with the appropriate wire header, and close the + * socket. They return true on success and false on any error (the error + * is also recorded via GpscStat). + */ class UDSConnector { public: + /* + * report_query -- send a SetQueryReq using the original 4-byte length header. + * + * Parameters: + * req -- the populated request message + * event -- human-readable event name used in error log messages + * config -- current connector config (UDS path, etc.) + */ bool static report_query(const gpsc::SetQueryReq &req, const std::string &event, const Config &config); + + /* + * report_per_node_batch -- send a yagpcc::SetPerNodeBatchReq using the + * 8-byte extended protocol header with request_type = 1. + * + * One whole plan-tree snapshot per call: a single socket open/write/close + * for the entire backend instead of one per node. + * + * Wire format: + * bytes 0-3: payload_size | kExtendedProtocolFlag (uint32 LE) + * bytes 4-5: request_type = 1 (uint16 LE) + * bytes 6-7: reserved = 0 (uint16 LE) + * bytes 8+: serialized SetPerNodeBatchReq + * + * Parameters: + * req -- the populated batch request message + * config -- current connector config (UDS path, etc.) + */ + bool static report_per_node_batch(const yagpcc::SetPerNodeBatchReq &req, + const Config &config); + + /* + * report_query_plan -- send a yagpcc::SetQueryPlanReq using the 8-byte + * extended protocol header with request_type = 2. + * + * Carries the coordinator-only ExplainPrintPlan document. Same framing as + * the other extended messages; only the request_type byte differs. + * + * Parameters: + * req -- the populated plan-doc request message + * config -- current connector config (UDS path, etc.) + */ + bool static report_query_plan(const yagpcc::SetQueryPlanReq &req, + const Config &config); }; #endif /* UDSCONNECTOR_H */ diff --git a/gpcontrib/gp_stats_collector/src/gp_stats_collector.c b/gpcontrib/gp_stats_collector/src/gp_stats_collector.c index d295e37b396..0a9ef782c89 100644 --- a/gpcontrib/gp_stats_collector/src/gp_stats_collector.c +++ b/gpcontrib/gp_stats_collector/src/gp_stats_collector.c @@ -31,6 +31,7 @@ #include "utils/builtins.h" #include "hook_wrappers.h" +#include "pg_query_state/pg_query_state.h" PG_MODULE_MAGIC; @@ -48,6 +49,12 @@ PG_FUNCTION_INFO_V1(gpsc_test_uds_stop_server); void _PG_init(void) { + /* + * Initialise the pg_query_state signal infrastructure unconditionally. + * It registers custom ProcSignal handlers and shared memory that must be + * set up during shared_preload_libraries processing. + */ + pg_qs_init(); if (Gp_role == GP_ROLE_DISPATCH || Gp_role == GP_ROLE_EXECUTE) hooks_init(); } diff --git a/gpcontrib/gp_stats_collector/src/hook_wrappers.cpp b/gpcontrib/gp_stats_collector/src/hook_wrappers.cpp index 38ea117bda2..7ce9dbcbb68 100644 --- a/gpcontrib/gp_stats_collector/src/hook_wrappers.cpp +++ b/gpcontrib/gp_stats_collector/src/hook_wrappers.cpp @@ -52,6 +52,7 @@ extern "C" { #include "GpscStat.h" #include "hook_wrappers.h" #include "memory/gpdbwrappers.h" +#include "pg_query_state/pg_query_state.h" static ExecutorStart_hook_type previous_ExecutorStart_hook = nullptr; static ExecutorRun_hook_type previous_ExecutorRun_hook = nullptr; @@ -95,16 +96,24 @@ static char *test_sock_path = NULL; static EventSender *sender = nullptr; +/* + * get_sender -- lazily construct the per-backend EventSender instance. + */ static inline EventSender * get_sender() { if (!sender) - { sender = new EventSender(); - } return sender; } +/* + * cpp_call -- invoke a C++ member function, converting exceptions to ereport. + * + * Wraps obj->*func(args...) in a try/catch so that C++ exceptions thrown + * inside EventSender methods are converted to PostgreSQL ERROR instead of + * crashing the backend with an unhandled exception. + */ template R cpp_call(T *obj, R (T::*func)(Args...), Args... args) @@ -115,11 +124,18 @@ cpp_call(T *obj, R (T::*func)(Args...), Args... args) } catch (const std::exception &e) { - ereport(ERROR, (errmsg("Unexpected exception in gpsc %s", e.what()))); + ereport(ERROR, (errmsg("Unexpected exception in gpsc: %s", e.what()))); pg_unreachable(); } } +/* + * hooks_init -- install all executor and utility hooks. + * + * Called from _PG_init() for QD and QE roles. Initialises the GUC registry, + * the GpscStat shared-memory statistics counters, and the stat-statements + * parser, then chains each hook onto the existing hook pointer. + */ void hooks_init() { @@ -148,6 +164,11 @@ hooks_init() ProcessUtility_hook = gpsc_process_utility_hook; } +/* + * hooks_deinit -- restore all hooks to their previous values. + * + * Called from _PG_fini(). Cleans up the EventSender and GpscStat resources. + */ void hooks_deinit() { @@ -166,32 +187,46 @@ hooks_deinit() if (sender) { delete sender; + sender = nullptr; } GpscStat::deinit(); ProcessUtility_hook = previous_ProcessUtility_hook; } +/* + * gpsc_ExecutorStart_hook -- wrapper for ExecutorStart. + * + * Notifies pg_query_state of the new query (enables instrumentation) and + * calls the EventSender before and after the actual ExecutorStart. + */ void gpsc_ExecutorStart_hook(QueryDesc *query_desc, int eflags) { + pg_qs_executor_start(query_desc, eflags); + cpp_call(get_sender(), &EventSender::executor_before_start, query_desc, eflags); + if (previous_ExecutorStart_hook) - { (*previous_ExecutorStart_hook)(query_desc, eflags); - } else - { standard_ExecutorStart(query_desc, eflags); - } + cpp_call(get_sender(), &EventSender::executor_after_start, query_desc, eflags); } +/* + * gpsc_ExecutorRun_hook -- wrapper for ExecutorRun. + * + * Pushes the QueryDesc onto the pg_query_state stack so signal handlers can + * find it, then pops it after the run (or on error). + */ void gpsc_ExecutorRun_hook(QueryDesc *query_desc, ScanDirection direction, uint64 count, bool execute_once) { + pg_qs_executor_run(query_desc); get_sender()->incr_depth(); PG_TRY(); { @@ -201,18 +236,27 @@ gpsc_ExecutorRun_hook(QueryDesc *query_desc, ScanDirection direction, else standard_ExecutorRun(query_desc, direction, count, execute_once); get_sender()->decr_depth(); + pg_qs_pop_query(); } PG_CATCH(); { get_sender()->decr_depth(); + pg_qs_pop_query(); PG_RE_THROW(); } PG_END_TRY(); } +/* + * gpsc_ExecutorFinish_hook -- wrapper for ExecutorFinish. + * + * Same push/pop pattern as ExecutorRun; keeps the QueryDesc visible to + * signal handlers during the finish phase. + */ void gpsc_ExecutorFinish_hook(QueryDesc *query_desc) { + pg_qs_executor_finish(query_desc); get_sender()->incr_depth(); PG_TRY(); { @@ -221,71 +265,88 @@ gpsc_ExecutorFinish_hook(QueryDesc *query_desc) else standard_ExecutorFinish(query_desc); get_sender()->decr_depth(); + pg_qs_pop_query(); } PG_CATCH(); { get_sender()->decr_depth(); + pg_qs_pop_query(); PG_RE_THROW(); } PG_END_TRY(); } +/* + * gpsc_ExecutorEnd_hook -- wrapper for ExecutorEnd. + * + * Notifies pg_query_state that the query is finishing (triggers the final + * plan-tree walk and LOG dump), then calls the EventSender and the standard + * end function. + */ void gpsc_ExecutorEnd_hook(QueryDesc *query_desc) { + pg_qs_executor_end(query_desc); cpp_call(get_sender(), &EventSender::executor_end, query_desc); if (previous_ExecutorEnd_hook) - { (*previous_ExecutorEnd_hook)(query_desc); - } else - { standard_ExecutorEnd(query_desc); - } } +/* + * gpsc_query_info_collect_hook -- wrapper for query_info_collect_hook. + */ void gpsc_query_info_collect_hook(QueryMetricsStatus status, void *arg) { cpp_call(get_sender(), &EventSender::query_metrics_collect, status, arg /* queryDesc */, false /* utility */, (ErrorData *) NULL); if (previous_query_info_collect_hook) - { (*previous_query_info_collect_hook)(status, arg); - } } #ifdef IC_TEARDOWN_HOOK +/* + * gpsc_ic_teardown_hook -- wrapper for ic_teardown_hook. + * + * Collects interconnect metrics when the motion layer tears down. + */ void gpsc_ic_teardown_hook(ChunkTransportState *transportStates, bool hasErrors) { cpp_call(get_sender(), &EventSender::ic_metrics_collect); if (previous_ic_teardown_hook) - { (*previous_ic_teardown_hook)(transportStates, hasErrors); - } } #endif #ifdef ANALYZE_STATS_COLLECT_HOOK +/* + * gpsc_analyze_stats_collect_hook -- wrapper for analyze_stats_collect_hook. + */ void gpsc_analyze_stats_collect_hook(QueryDesc *query_desc) { cpp_call(get_sender(), &EventSender::analyze_stats_collect, query_desc); if (previous_analyze_stats_collect_hook) - { (*previous_analyze_stats_collect_hook)(query_desc); - } } #endif +/* + * gpsc_process_utility_hook -- wrapper for ProcessUtility_hook. + * + * Constructs a minimal QueryDesc from the utility statement to reuse the + * existing EventSender::query_metrics_collect interface. The QueryDesc is + * freed in both the success and error paths. + */ static void gpsc_process_utility_hook(PlannedStmt *pstmt, const char *queryString, bool readOnlyTree, ProcessUtilityContext context, ParamListInfo params, QueryEnvironment *queryEnv, DestReceiver *dest, QueryCompletion *qc) { - /* Project utility data on QueryDesc to use existing logic */ QueryDesc *query_desc = (QueryDesc *) palloc0(sizeof(QueryDesc)); query_desc->sourceText = queryString; @@ -297,31 +358,26 @@ gpsc_process_utility_hook(PlannedStmt *pstmt, const char *queryString, PG_TRY(); { if (previous_ProcessUtility_hook) - { (*previous_ProcessUtility_hook)(pstmt, queryString, readOnlyTree, context, params, queryEnv, dest, qc); - } else - { standard_ProcessUtility(pstmt, queryString, readOnlyTree, context, params, queryEnv, dest, qc); - } get_sender()->decr_depth(); cpp_call(get_sender(), &EventSender::query_metrics_collect, METRICS_QUERY_DONE, (void *) query_desc, true /* utility */, (ErrorData *) NULL); - pfree(query_desc); } PG_CATCH(); { - ErrorData *edata; - MemoryContext oldctx; + ErrorData *edata; + MemoryContext oldctx; oldctx = MemoryContextSwitchTo(TopMemoryContext); - edata = CopyErrorData(); + edata = CopyErrorData(); FlushErrorState(); MemoryContextSwitchTo(oldctx); @@ -329,24 +385,31 @@ gpsc_process_utility_hook(PlannedStmt *pstmt, const char *queryString, cpp_call(get_sender(), &EventSender::query_metrics_collect, METRICS_QUERY_ERROR, (void *) query_desc, true /* utility */, edata); - pfree(query_desc); ReThrowError(edata); } PG_END_TRY(); } +/* + * check_stats_loaded -- raise ERROR if GpscStat shared memory is not mapped. + * + * Called by SQL-callable stat functions to guard against use before the + * extension was loaded via shared_preload_libraries. + */ static void check_stats_loaded() { if (!GpscStat::loaded()) - { - ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("gp_stats_collector must be loaded via " - "shared_preload_libraries"))); - } + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("gp_stats_collector must be loaded via " + "shared_preload_libraries"))); } +/* + * gpsc_functions_reset -- reset all GpscStat counters to zero. + */ void gpsc_functions_reset() { @@ -354,6 +417,13 @@ gpsc_functions_reset() GpscStat::reset(); } +/* + * gpsc_functions_get -- return the current GpscStat counters as a tuple. + * + * Returns one row with columns: + * segid, total_messages, send_failures, connection_failures, + * other_errors, max_message_size. + */ Datum gpsc_functions_get(FunctionCallInfo fcinfo) { @@ -361,21 +431,15 @@ gpsc_functions_get(FunctionCallInfo fcinfo) check_stats_loaded(); auto stats = GpscStat::get_stats(); TupleDesc tupdesc = CreateTemplateTupleDesc(ATTNUM); - TupleDescInitEntry(tupdesc, (AttrNumber) 1, "segid", INT4OID, - -1 /* typmod */, 0 /* attdim */); - TupleDescInitEntry(tupdesc, (AttrNumber) 2, "total_messages", INT8OID, - -1 /* typmod */, 0 /* attdim */); - TupleDescInitEntry(tupdesc, (AttrNumber) 3, "send_failures", INT8OID, - -1 /* typmod */, 0 /* attdim */); - TupleDescInitEntry(tupdesc, (AttrNumber) 4, "connection_failures", INT8OID, - -1 /* typmod */, 0 /* attdim */); - TupleDescInitEntry(tupdesc, (AttrNumber) 5, "other_errors", INT8OID, - -1 /* typmod */, 0 /* attdim */); - TupleDescInitEntry(tupdesc, (AttrNumber) 6, "max_message_size", INT4OID, - -1 /* typmod */, 0 /* attdim */); + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "segid", INT4OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 2, "total_messages", INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 3, "send_failures", INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 4, "connection_failures", INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 5, "other_errors", INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 6, "max_message_size", INT4OID, -1, 0); tupdesc = BlessTupleDesc(tupdesc); Datum values[ATTNUM]; - bool nulls[ATTNUM]; + bool nulls[ATTNUM]; MemSet(nulls, 0, sizeof(nulls)); values[0] = Int32GetDatum(GpIdentity.segindex); values[1] = Int64GetDatum(stats.total); @@ -384,10 +448,12 @@ gpsc_functions_get(FunctionCallInfo fcinfo) values[4] = Int64GetDatum(stats.failed_other); values[5] = Int32GetDatum(stats.max_message_size); HeapTuple tuple = gpdb::heap_form_tuple(tupdesc, values, nulls); - Datum result = HeapTupleGetDatum(tuple); - PG_RETURN_DATUM(result); + PG_RETURN_DATUM(HeapTupleGetDatum(tuple)); } +/* + * test_uds_stop_server -- close and remove the test Unix-domain socket server. + */ void test_uds_stop_server() { @@ -404,13 +470,19 @@ test_uds_stop_server() } } +/* + * test_uds_start_server -- create a listening Unix-domain socket at path. + * + * Intended for integration tests that verify the UDS connector end-to-end. + * Raises ERROR if the socket cannot be created or bound. + */ void test_uds_start_server(const char *path) { struct sockaddr_un addr = {.sun_family = AF_UNIX}; if (strlen(path) >= sizeof(addr.sun_path)) - ereport(ERROR, (errmsg("path too long"))); + ereport(ERROR, (errmsg("Unix socket path too long"))); test_uds_stop_server(); @@ -423,20 +495,27 @@ test_uds_start_server(const char *path) listen(test_server_fd, TEST_MAX_CONNECTIONS) < 0) { test_uds_stop_server(); - ereport(ERROR, (errmsg("socket setup failed: %m"))); + ereport(ERROR, (errmsg("test UDS socket setup failed: %m"))); } } +/* + * test_uds_receive -- accept one connection and count bytes received. + * + * Polls for an incoming connection with a deadline of timeout_ms milliseconds. + * Returns the total number of bytes received, or 0 on timeout. + * Raises ERROR on poll/accept failures. + */ int64 test_uds_receive(int timeout_ms) { - char buf[TEST_RCV_BUF_SIZE]; - int rc; + char buf[TEST_RCV_BUF_SIZE]; + int rc; struct pollfd pfd = {.fd = test_server_fd, .events = POLLIN}; - int64 total = 0; + int64 total = 0; if (test_server_fd < 0) - ereport(ERROR, (errmsg("server not started"))); + ereport(ERROR, (errmsg("test UDS server not started"))); for (;;) { @@ -453,7 +532,7 @@ test_uds_receive(int timeout_ms) if (pfd.revents & POLLIN) { - int client = accept(test_server_fd, NULL, NULL); + int client = accept(test_server_fd, NULL, NULL); ssize_t n; if (client < 0) @@ -466,9 +545,8 @@ test_uds_receive(int timeout_ms) else if (errno != EINTR) break; } - close(client); } return total; -} \ No newline at end of file +} diff --git a/gpcontrib/gp_stats_collector/src/pg_query_state/pg_query_state.c b/gpcontrib/gp_stats_collector/src/pg_query_state/pg_query_state.c new file mode 100644 index 00000000000..483c980ff17 --- /dev/null +++ b/gpcontrib/gp_stats_collector/src/pg_query_state/pg_query_state.c @@ -0,0 +1,1111 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * pg_query_state.c + * Core of the pg_query_state signal-dispatch layer. + * + * This module provides: + * - Shared-memory setup (shm_toc segment with params, mq, mq_req_id). + * - Custom ProcSignal registrations for three signals: + * QueryStatePollReason -> SendQueryState() + * BackendInfoPollReason -> SendCdbComponents() + * - GUC variables: pg_query_state.enable / enable_timing / enable_buffers. + * - Executor lifecycle hooks (start/run/finish/end) that maintain the + * QueryDescStack and enable instrumentation on the top-level query. + * - A requestor-side helper: shm_mq_receive_with_timeout(). + * + * Per-node stats are pushed to the yagpcc UDS sink on demand, when a backend is + * signalled to report its live query state; see signal_handler.c. + * + * Portions derived from pg_query_state + * (https://github.com/postgrespro/pg_query_state), under the PostgreSQL + * License: + * Portions Copyright (c) 2016-2025, Postgres Professional + * + * IDENTIFICATION + * gpcontrib/gp_stats_collector/src/pg_query_state/pg_query_state.c + * + *------------------------------------------------------------------------- + */ + +#include "pg_query_state.h" +#include "PlanNodeEmitter.h" + +#include "access/htup_details.h" +#include "access/xact.h" +#include "catalog/pg_type.h" +#include "cdb/cdbdispatchresult.h" +#include "cdb/cdbdisp_query.h" +#include "cdb/cdbexplain.h" +#include "cdb/cdbvars.h" +#include "executor/execParallel.h" +#include "executor/executor.h" +#include "fmgr.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "nodes/nodeFuncs.h" +#include "nodes/print.h" +#include "parser/analyze.h" +#include "pgstat.h" +#include "postmaster/bgworker.h" +#include "storage/ipc.h" +#include "storage/s_lock.h" +#include "storage/spin.h" +#include "storage/procarray.h" +#include "storage/procsignal.h" +#include "storage/shm_toc.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/timestamp.h" +#include "utils/lsyscache.h" +#include "utils/portal.h" +#include "utils/typcache.h" + +#define TEXT_CSTR_CMP(text, cstr) \ + (memcmp(VARDATA(text), (cstr), VARSIZE(text) - VARHDRSZ)) + +/* GUC variables */ +/* Master switch: disabling this suppresses all stat collection. */ +bool pg_qs_enable = true; + +/* Collect timing (wall-clock) data in addition to row counts. */ +bool pg_qs_timing = true; + +/* Collect buffer usage via Instrumentation.bufusage. */ +bool pg_qs_buffers = true; + +/* + * Rolling counter incremented for every QueryDesc pushed onto the stack. + * Used to generate synthetic queryId values for statements lacking one. + */ +static int qs_query_count = 0; + +/* Saved hook pointer for chaining shmem_startup callbacks. */ +static shmem_startup_hook_type prev_shmem_startup_hook = NULL; + +/* Whether pg_qs_shmem_startup has completed successfully. */ +static bool module_initialized = false; + +/* + * Monotonically increasing request counter on the requestor side. + * Compared against *mq_req_id in the reply to detect stale responses. + */ +static int reqid = 0; + +/* Shared-memory variables (pointers into the shm_toc segment) */ +/* Table of contents anchoring the whole shared segment. */ +static shm_toc *toc = NULL; + +/* + * Signal parameters written by the requestor and read by the handler. + * Slot 0 in the toc. + */ +pg_qs_params *params = NULL; + +/* + * Raw shared memory queue used to return data from the handler. + * Slot 1 in the toc. + */ +shm_mq *mq = NULL; + +/* + * Shared request-id counter. The requestor increments it before sending a + * signal; the handler echoes it back so the requestor can detect stale + * replies. Slot 2 in the toc. + */ +uint32 *mq_req_id = NULL; + +/* + * Per-backend trace_id slots (toc key 3), indexed by BackendId. The dispatcher + * stamps the target's slot before signalling; the signaled backend reads its + * own slot to key the batch it pushes. See the header for the full rationale. + */ +char (*qs_trace_slots)[GPSC_TRACE_ID_LEN] = NULL; + +/* Global signal-reason handles (set during pg_qs_init) */ +List *QueryDescStack = NIL; + +ProcSignalReason QueryStatePollReason = INVALID_PROCSIGNAL; +ProcSignalReason BackendInfoPollReason = INVALID_PROCSIGNAL; + +/* Forward declarations for module-private helpers */ +static Size pg_qs_shmem_size(void); +static void pg_qs_shmem_startup(void); +static void push_query(QueryDesc *queryDesc); + +static List *get_query_backend_info(ArrayType *array); + +static shm_mq_result receive_msg_by_parts(shm_mq_handle *mqh, Size *total, + void **datap, int64 timeout, + int *rc, bool nowait); +static PG_QS_RequestResult GetRemoteBackendInfo(PGPROC *proc, List **result); +static void CollectQEQueryState(List *backendInfo, bytea *trace_id); + +#if PG_VERSION_NUM >= 150000 +static shmem_request_hook_type prev_shmem_request_hook = NULL; +static void pg_qs_shmem_request(void); +#endif + +/* + * pg_qs_shmem_size -- compute the size of the shared memory segment. + * + * The segment holds four objects at fixed toc keys: + * key 0: pg_qs_params + * key 1: message queue of QUEUE_SIZE bytes + * key 2: uint32 request-id counter + * key 3: per-backend trace_id slots, char[GPSC_TRACE_ID_LEN] × (MaxBackends+1) + */ +static Size +pg_qs_shmem_size(void) +{ + shm_toc_estimator e; + Size size; + int nkeys = 4; + + shm_toc_initialize_estimator(&e); + shm_toc_estimate_chunk(&e, sizeof(pg_qs_params)); + shm_toc_estimate_chunk(&e, (Size) QUEUE_SIZE); + shm_toc_estimate_chunk(&e, sizeof(uint32)); + shm_toc_estimate_chunk(&e, (Size) GPSC_TRACE_ID_LEN * (MaxBackends + 1)); + shm_toc_estimate_keys(&e, nkeys); + size = shm_toc_estimate(&e); + return size; +} + +/* + * pg_qs_shmem_startup -- attach to (or initialize) the shared segment. + * + * Called from the shmem_startup_hook chain after shared memory is mapped. + * On first call (found == false) it initialises all sub-structures. + * On subsequent calls it just re-attaches the toc pointers. + */ +static void +pg_qs_shmem_startup(void) +{ + bool found; + Size shmem_size = pg_qs_shmem_size(); + void *shmem; + int num_toc = 0; + + LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); + shmem = ShmemInitStruct("pg_query_state", shmem_size, &found); + if (!found) + { + toc = shm_toc_create(PG_QS_MODULE_KEY, shmem, shmem_size); + + params = shm_toc_allocate(toc, sizeof(pg_qs_params)); + shm_toc_insert(toc, num_toc++, params); + + mq = shm_toc_allocate(toc, QUEUE_SIZE); + shm_toc_insert(toc, num_toc++, mq); + + mq_req_id = shm_toc_allocate(toc, sizeof(uint32)); + shm_toc_insert(toc, num_toc++, mq_req_id); + *mq_req_id = 0; + + qs_trace_slots = shm_toc_allocate(toc, + (Size) GPSC_TRACE_ID_LEN * (MaxBackends + 1)); + shm_toc_insert(toc, num_toc++, qs_trace_slots); + memset(qs_trace_slots, 0, (Size) GPSC_TRACE_ID_LEN * (MaxBackends + 1)); + } + else + { + toc = shm_toc_attach(PG_QS_MODULE_KEY, shmem); + params = shm_toc_lookup(toc, num_toc++, false); + mq = shm_toc_lookup(toc, num_toc++, false); + mq_req_id = shm_toc_lookup(toc, num_toc++, false); + qs_trace_slots = shm_toc_lookup(toc, num_toc++, false); + } + LWLockRelease(AddinShmemInitLock); + + if (prev_shmem_startup_hook) + prev_shmem_startup_hook(); + + module_initialized = true; +} + +#if PG_VERSION_NUM >= 150000 +/* + * pg_qs_shmem_request -- hook called to request shared memory space. + * + * PostgreSQL 15+ separates the request phase from the startup phase. + * This hook is installed only when building against PG15+. + */ +static void +pg_qs_shmem_request(void) +{ + if (prev_shmem_request_hook) + prev_shmem_request_hook(); + + RequestAddinShmemSpace(pg_qs_shmem_size()); +} +#endif + +/* + * pg_qs_init -- initialise the pg_query_state signal infrastructure. + * + * Must be called from _PG_init() while process_shared_preload_libraries_in_progress + * is true. Registers shared memory, custom ProcSignal handlers and GUC + * variables. Safe to call unconditionally for all roles. + */ +void +pg_qs_init(void) +{ + if (!process_shared_preload_libraries_in_progress) + return; + +#if PG_VERSION_NUM >= 150000 + prev_shmem_request_hook = shmem_request_hook; + shmem_request_hook = pg_qs_shmem_request; +#else + RequestAddinShmemSpace(pg_qs_shmem_size()); +#endif + + QueryStatePollReason = RegisterCustomProcSignalHandler(SendQueryState); + BackendInfoPollReason = RegisterCustomProcSignalHandler(SendCdbComponents); + + if (QueryStatePollReason == INVALID_PROCSIGNAL || + BackendInfoPollReason == INVALID_PROCSIGNAL) + { + ereport(WARNING, (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("pg_query_state isn't loaded: insufficient custom ProcSignal slots"))); + return; + } + + DefineCustomBoolVariable("pg_query_state.enable", + "Enable module.", + NULL, + &pg_qs_enable, + true, + PGC_SUSET, + 0, + NULL, NULL, NULL); + + DefineCustomBoolVariable("pg_query_state.enable_timing", + "Collect timing data, not just row counts.", + NULL, + &pg_qs_timing, + true, + PGC_SUSET, + 0, + NULL, NULL, NULL); + + DefineCustomBoolVariable("pg_query_state.enable_buffers", + "Collect buffer usage.", + NULL, + &pg_qs_buffers, + true, + PGC_SUSET, + 0, + NULL, NULL, NULL); + + prev_shmem_startup_hook = shmem_startup_hook; + shmem_startup_hook = pg_qs_shmem_startup; + + elog(LOG, "pg_query_state: signal infrastructure initialised"); +} + +/* Executor lifecycle hooks */ +/* + * pg_qs_executor_start -- called at the start of executor execution. + * + * Enables instrumentation on the QueryDesc when: + * - pg_query_state is enabled + * - this is not an EXPLAIN-only execution + * - we are on a QD or QE role + * - there is no outer query already on the stack (top-level only) + * - the query passes the filter + * - no showstatctx is already attached + * + * Also assigns a synthetic queryId when the planner left it as zero. + * + * Parameters: + * queryDesc -- the QueryDesc being started + * eflags -- executor flags (EXEC_FLAG_EXPLAIN_ONLY etc.) + */ +void +pg_qs_executor_start(QueryDesc *queryDesc, int eflags) +{ + instr_time starttime; + + if (pg_qs_enable + && ((eflags & EXEC_FLAG_EXPLAIN_ONLY) == 0) + && (Gp_role == GP_ROLE_DISPATCH || Gp_role == GP_ROLE_EXECUTE) + && is_querystack_empty() + && filter_query(queryDesc) + && queryDesc->showstatctx == NULL) + { + queryDesc->instrument_options |= INSTRUMENT_CDB; + queryDesc->instrument_options |= INSTRUMENT_ROWS; + if (pg_qs_timing) + queryDesc->instrument_options |= INSTRUMENT_TIMER; + if (pg_qs_buffers) + queryDesc->instrument_options |= INSTRUMENT_BUFFERS; + + INSTR_TIME_SET_CURRENT(starttime); + + /* + * cdbexplain_showExecStatsBegin() aggregates QE stats on the QD and + * asserts Gp_role != GP_ROLE_EXECUTE, so it must run on the dispatcher + * only. QE backends still get instrument_options above, which is all + * the per-node walker reads. + */ + if (Gp_role == GP_ROLE_DISPATCH) + queryDesc->showstatctx = + cdbexplain_showExecStatsBegin(queryDesc, starttime); + queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false); + + gpsc_reset_node_roll_state(); + } + + if (queryDesc->plannedstmt->queryId == 0) + queryDesc->plannedstmt->queryId = + ((uint64) gp_command_count << 32) + qs_query_count; +} + +/* + * pg_qs_executor_run -- called when the executor begins fetching tuples. + * + * Pushes the QueryDesc onto the stack so signal handlers can find it. + */ +void +pg_qs_executor_run(QueryDesc *queryDesc) +{ + push_query(queryDesc); +} + +/* + * pg_qs_executor_finish -- called after all tuples have been fetched. + * + * Pushes the QueryDesc again to keep the stack consistent during the finish + * phase (needed so signal handlers still see the query during cleanup). + */ +void +pg_qs_executor_finish(QueryDesc *queryDesc) +{ + push_query(queryDesc); +} + +/* + * pg_qs_executor_end -- called when executor resources are released. + * + * Drops the per-node rolling state so the next query on this backend starts its + * delta accounting clean. It does not collect or push anything: a finish is not + * a signalled collection and carries no trace_id to key a batch under. + */ +void +pg_qs_executor_end(QueryDesc *queryDesc) +{ + if (queryDesc && pg_qs_enable) + gpsc_reset_node_roll_state(); +} + +static void +push_query(QueryDesc *queryDesc) +{ + qs_query_count++; + QueryDescStack = lcons(queryDesc, QueryDescStack); +} + +/* + * pg_qs_push_query -- public alias for push_query, called from hook_wrappers. + */ +void +pg_qs_push_query(QueryDesc *queryDesc) +{ + qs_query_count++; + QueryDescStack = lcons(queryDesc, QueryDescStack); +} + +/* + * pg_qs_pop_query -- remove the most-recently-pushed QueryDesc from the stack. + */ +void +pg_qs_pop_query(void) +{ + QueryDescStack = list_delete_first(QueryDescStack); +} + +bool +is_querystack_empty(void) +{ + return list_length(QueryDescStack) == 0; +} + +QueryDesc * +get_toppest_query(void) +{ + return (QueryDescStack == NIL) ? NULL : (QueryDesc *) llast(QueryDescStack); +} + +/* + * filter_query -- decide whether to instrument a given QueryDesc. + * + * Returns false for cursor queries with non-default cursor options, and for + * utility statements. Returns true for SELECT, INSERT, UPDATE, DELETE. + */ +bool +filter_query(QueryDesc *queryDesc) +{ + Portal portal; + + if (queryDesc == NULL) + return false; + + if (queryDesc->extended_query && queryDesc->portal_name) + { + portal = GetPortalByName(queryDesc->portal_name); + if (!PointerIsValid(portal) || portal->cursorOptions != CURSOR_OPT_NO_SCROLL) + return false; + } + + return (queryDesc->operation == CMD_SELECT || + queryDesc->operation == CMD_DELETE || + queryDesc->operation == CMD_INSERT || + queryDesc->operation == CMD_UPDATE); +} + +/* + * wait_for_mq_detached -- spin until the caller has attached to the mq or + * MAX_SND_TIMEOUT milliseconds elapses. + * + * Returns true if the queue was detached within the timeout (i.e. the other + * end is done), false on timeout. + */ +bool +wait_for_mq_detached(shm_mq_handle *mqh) +{ + instr_time start_time; + instr_time cur_time; + int64 delay = MAX_SND_TIMEOUT; + + INSTR_TIME_SET_CURRENT(start_time); + for (;;) + { + if (shm_mq_wait_for_attach(mqh) == SHM_MQ_DETACHED) + break; + WaitLatch(MyLatch, + WL_LATCH_SET | WL_EXIT_ON_PM_DEATH | WL_TIMEOUT, + delay, PG_WAIT_IPC); + INSTR_TIME_SET_CURRENT(cur_time); + INSTR_TIME_SUBTRACT(cur_time, start_time); + delay = MAX_SND_TIMEOUT - (int64) INSTR_TIME_GET_MILLISEC(cur_time); + if (delay <= 0) + { + elog(WARNING, "pg_query_state: wait_for_mq_detached timed out"); + return false; + } + CHECK_FOR_INTERRUPTS(); + } + return true; +} + +/* + * LockShmem -- acquire an exclusive user-lock keyed by (PG_QS_MODULE_KEY, key). + * + * Used to serialise access to the shared mq between concurrent requestors + * and between requestor and handler. + */ +void +LockShmem(LOCKTAG *tag, uint32 key) +{ + LockAcquireResult result; + + tag->locktag_field1 = PG_QS_MODULE_KEY; + tag->locktag_field2 = key; + tag->locktag_field3 = 0; + tag->locktag_field4 = 0; + tag->locktag_type = LOCKTAG_USERLOCK; + tag->locktag_lockmethodid = USER_LOCKMETHOD; + + result = LockAcquire(tag, ExclusiveLock, false, false); + Assert(result == LOCKACQUIRE_OK); +} + +/* + * UnlockShmem -- release the exclusive user-lock acquired by LockShmem. + */ +void +UnlockShmem(LOCKTAG *tag) +{ + LockRelease(tag, ExclusiveLock, false); +} + +/* + * GetRemoteBackendInfo -- obtain the list of (segid, pid) pairs from QD. + * + * Sends BackendInfoPollReason to proc and waits for the reply. On success, + * *result is populated with gp_segment_pid entries (palloc'd). + * + * Returns the PG_QS_RequestResult code from the reply. + */ +static PG_QS_RequestResult +GetRemoteBackendInfo(PGPROC *proc, List **result) +{ + int sig_result; + shm_mq_handle *mqh; + shm_mq_result mq_receive_result; + Size msg_len; + backend_info *msg; + LOCKTAG tag; + int i; + + LockShmem(&tag, PG_QS_SND_KEY); + params->reason = BackendInfoPollReason; + mq = shm_mq_create(mq, QUEUE_SIZE); + shm_mq_set_sender(mq, proc); + shm_mq_set_receiver(mq, MyProc); + *mq_req_id = reqid; + UnlockShmem(&tag); + + sig_result = SendProcSignal(proc->pid, BackendInfoPollReason, + proc->backendId); + if (sig_result == -1) + ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not send BackendInfoPollReason signal"))); + + mqh = shm_mq_attach(mq, NULL, NULL); + mq_receive_result = shm_mq_receive_with_timeout(mqh, &msg_len, + (void **) &msg, + MAX_RCV_TIMEOUT); + + if (mq_receive_result != SHM_MQ_SUCCESS || msg == NULL || + msg->reqid != (uint32) reqid) + { + shm_mq_detach(mqh); + ereport(WARNING, (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("GetRemoteBackendInfo: message not received"))); + return QUERY_NOT_RUNNING; + } + + if (msg->result_code != QS_RETURNED) + { + PG_QS_RequestResult result_code = msg->result_code; + shm_mq_detach(mqh); + return result_code; + } + + /* Validate the reply payload length against the reported backend count. */ + { + int expected_len = BASE_SIZEOF_GP_BACKEND_INFO + + msg->number * sizeof(gp_segment_pid); + if ((int) msg_len != expected_len) + { + shm_mq_detach(mqh); + ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("GetRemoteBackendInfo: unexpected message length"))); + } + } + + for (i = 0; i < msg->number; i++) + { + gp_segment_pid *segpid = palloc(sizeof(gp_segment_pid)); + *segpid = msg->pids[i]; + *result = lcons(segpid, *result); + } + + shm_mq_detach(mqh); + return QS_RETURNED; +} + +/* + * CollectQEQueryState -- fan-out query-state signals to all QE backends. + * + * Dispatches a cbdb_mpp_query_state() call to each segment listed in + * backendInfo. Results are returned as raw CdbPgResults. + */ +static void +CollectQEQueryState(List *backendInfo, bytea *trace_id) +{ + ListCell *lc; + int index = 0; + StringInfoData params_buf; + char *sql; + char trace_id_hex[2 * GPSC_TRACE_ID_LEN + 1]; + + if (list_length(backendInfo) == 0) + return; + + initStringInfo(¶ms_buf); + + foreach(lc, backendInfo) + { + gp_segment_pid *segpid = (gp_segment_pid *) lfirst(lc); + index++; + appendStringInfo(¶ms_buf, "'(%d,%d)'", segpid->segid, segpid->pid); + if (index != list_length(backendInfo)) + appendStringInfoChar(¶ms_buf, ','); + } + + hex_encode(VARDATA_ANY(trace_id), GPSC_TRACE_ID_LEN, trace_id_hex); + trace_id_hex[2 * GPSC_TRACE_ID_LEN] = '\0'; + sql = psprintf("SELECT gpsc.cbdb_mpp_query_state((ARRAY[%s])::gpsc.gp_segment_pid[], '\\x%s'::bytea)", + params_buf.data, trace_id_hex); + + CdbDispatchCommand(sql, DF_NONE, NULL); + pfree(params_buf.data); + pfree(sql); +} + +/* + * shm_mq_receive_with_timeout -- receive from mqh, blocking up to `timeout` ms. + * + * Calls receive_msg_by_parts() in a loop, sleeping on the latch between + * retries. Returns SHM_MQ_SUCCESS, SHM_MQ_DETACHED, or SHM_MQ_WOULD_BLOCK + * (the last meaning the timeout expired). + * + * On success, *nbytesp is set to the message length and *datap to a palloc'd + * buffer containing the message. + */ +shm_mq_result +shm_mq_receive_with_timeout(shm_mq_handle *mqh, + Size *nbytesp, + void **datap, + int64 timeout) +{ + int rc = 0; + int64 delay = timeout; + instr_time start_time; + instr_time cur_time; + + INSTR_TIME_SET_CURRENT(start_time); + + for (;;) + { + shm_mq_result result; + + result = receive_msg_by_parts(mqh, nbytesp, datap, timeout, &rc, true); + if (result != SHM_MQ_WOULD_BLOCK) + return result; + + if (rc & WL_TIMEOUT || delay <= 0) + return SHM_MQ_WOULD_BLOCK; + + rc = WaitLatch(MyLatch, + WL_LATCH_SET | WL_EXIT_ON_PM_DEATH | WL_TIMEOUT, + delay, PG_WAIT_EXTENSION); + + INSTR_TIME_SET_CURRENT(cur_time); + INSTR_TIME_SUBTRACT(cur_time, start_time); + delay = timeout - (int64) INSTR_TIME_GET_MILLISEC(cur_time); + if (delay <= 0) + return SHM_MQ_WOULD_BLOCK; + + CHECK_FOR_INTERRUPTS(); + ResetLatch(MyLatch); + } +} + +/* + * receive_msg_by_parts -- reassemble a multi-chunk message from mqh. + * + * The wire protocol prefixes each message with its total byte count (a Size), + * followed by one or more chunks of up to MSG_MAX_SIZE bytes. This function + * reads the prefix, allocates a buffer, and loops until all chunks arrive. + * + * Parameters: + * mqh -- attached message-queue handle + * total -- out: total bytes received + * datap -- out: palloc'd buffer with reassembled message + * timeout -- caller's deadline in ms (used only for PART_RCV_DELAY retries) + * rc -- out: WaitLatch flags (set to WL_TIMEOUT if we give up) + * nowait -- passed through to shm_mq_receive + */ +static shm_mq_result +receive_msg_by_parts(shm_mq_handle *mqh, Size *total, void **datap, + int64 timeout, int *rc, bool nowait) +{ + shm_mq_result mq_receive_result; + shm_mq_msg *buff; + int offset; + Size *expected; + Size expected_data; + Size len; + + /* Read the length prefix. */ + mq_receive_result = shm_mq_receive(mqh, &len, (void **) &expected, nowait); + if (mq_receive_result != SHM_MQ_SUCCESS) + return mq_receive_result; + Assert(len == sizeof(Size)); + + expected_data = *expected; + Assert(expected_data < UINT32_MAX); + *datap = palloc0(expected_data); + + /* Reassemble chunks until we have expected_data bytes. */ + for (offset = 0; offset < (int) expected_data; ) + { + int64 delay = timeout; + + for (;;) + { + mq_receive_result = shm_mq_receive(mqh, &len, (void **) &buff, + nowait); + if (mq_receive_result != SHM_MQ_SUCCESS) + { + if (nowait && mq_receive_result == SHM_MQ_WOULD_BLOCK) + { + if (delay > 0) + { + pg_usleep(PART_RCV_DELAY * 1000); + delay -= PART_RCV_DELAY; + continue; + } + if (rc) + *rc |= WL_TIMEOUT; + } + return mq_receive_result; + } + break; + } + memcpy((char *) *datap + offset, buff, len); + offset += len; + } + + *total = offset; + return mq_receive_result; +} + +/* SQL callable functions */ +/* + * pg_query_state -- entry point for the pg_query_state() SQL function. + * + * Obtains the user-id and segment-backend list from the target backend, + * then fans out cbdb_mpp_query_state() to each QE. + */ +PG_FUNCTION_INFO_V1(pg_query_state); +Datum +pg_query_state(PG_FUNCTION_ARGS) +{ + pid_t pid = PG_GETARG_INT32(0); + bytea *trace_id = PG_GETARG_BYTEA_P(1); + PGPROC *proc; + LOCKTAG tag; + PG_QS_RequestResult result; + List *backend_info = NIL; + + if (VARSIZE_ANY_EXHDR(trace_id) != GPSC_TRACE_ID_LEN) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid size of trace_id: %zu, expected %d", + VARSIZE_ANY_EXHDR(trace_id), GPSC_TRACE_ID_LEN))); + + if (pid == MyProcPid) + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot extract state of current process"))); + + if (!module_initialized) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("pg_query_state must be loaded via shared_preload_libraries"))); + + proc = BackendPidGetProc(pid); + if (!proc || proc->backendId == InvalidBackendId || + proc->databaseId == InvalidOid || proc->roleId == InvalidOid) + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("backend with pid=%d not found", pid))); + + if (!(superuser() || GetUserId() == proc->roleId)) + { + ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied"))); + } + + LockShmem(&tag, PG_QS_RCV_KEY); + PG_TRY(); + { + reqid = *mq_req_id + 1; + result = GetRemoteBackendInfo(proc, &backend_info); + UnlockShmem(&tag); + } + PG_CATCH(); + { + UnlockShmem(&tag); + PG_RE_THROW(); + } + PG_END_TRY(); + + switch (result) + { + case QUERY_NOT_RUNNING: + elog(DEBUG1, "pg_query_state: pid=%d is not running a query", pid); + break; + + case STAT_DISABLED: + elog(DEBUG1, "pg_query_state: stats collection disabled"); + break; + + case QS_RETURNED: + /* + * Signal all segment QEs to push their plan-node stats via UDS, + * carrying the trace_id so every backend's batch lands under the one + * key this pg_query_state() invocation owns. + */ + CollectQEQueryState(backend_info, trace_id); + + /* + * Signal the QD backend itself so it pushes coordinator-side plan + * nodes and the plan-doc. SendQueryState() emits directly via UDS. + * Stamp the target's own trace slot before signalling, so its batch + * lands under this collection's key. + */ + memcpy(qs_trace_slots[proc->backendId], VARDATA_ANY(trace_id), + GPSC_TRACE_ID_LEN); + SendProcSignal(proc->pid, QueryStatePollReason, proc->backendId); + break; + } + + PG_RETURN_VOID(); +} + +/* + * pg_query_state_backends -- list the QE backends participating in the query + * running on backend `pid`. + * + * Returns a set of (segid, pid) rows obtained from the coordinator via + * GetRemoteBackendInfo (the same list the poll path fans out to). A consumer + * can use the row count as the expected number of backends that will report. + * + * Uses the materialize SRF mode: the whole list is built into a tuplestore in + * one call. Returns an empty set when the target query is not running. + */ +PG_FUNCTION_INFO_V1(pg_query_state_backends); +Datum +pg_query_state_backends(PG_FUNCTION_ARGS) +{ + pid_t pid = PG_GETARG_INT32(0); + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + TupleDesc tupdesc; + Tuplestorestate *tupstore; + MemoryContext per_query_ctx; + MemoryContext oldcontext; + PGPROC *proc; + List *backend_info = NIL; + LOCKTAG tag; + PG_QS_RequestResult info_result; + ListCell *lc; + + /* Standard set-returning-function materialize-mode preamble. */ + if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsinfo->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialize mode required, but it is not allowed in this context"))); + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("function returning record called in context that cannot accept type record"))); + + per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; + oldcontext = MemoryContextSwitchTo(per_query_ctx); + tupstore = tuplestore_begin_heap(true, false, work_mem); + rsinfo->returnMode = SFRM_Materialize; + rsinfo->setResult = tupstore; + rsinfo->setDesc = tupdesc; + MemoryContextSwitchTo(oldcontext); + + if (pid == MyProcPid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot extract state of current process"))); + + proc = BackendPidGetProc(pid); + if (!proc || proc->backendId == InvalidBackendId || + proc->databaseId == InvalidOid || proc->roleId == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("backend with pid=%d not found", pid))); + + if (!module_initialized) + { + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("pg_query_state must be loaded via shared_preload_libraries"))); + } + + if (!(superuser() || GetUserId() == proc->roleId)) + { + ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied"))); + } + + LockShmem(&tag, PG_QS_RCV_KEY); + PG_TRY(); + { + reqid = *mq_req_id + 1; + info_result = GetRemoteBackendInfo(proc, &backend_info); + UnlockShmem(&tag); + } + PG_CATCH(); + { + UnlockShmem(&tag); + PG_RE_THROW(); + } + PG_END_TRY(); + + /* Not running / disabled: return an empty set rather than erroring. */ + if (info_result != QS_RETURNED) + return (Datum) 0; + + foreach(lc, backend_info) + { + gp_segment_pid *segpid = (gp_segment_pid *) lfirst(lc); + Datum values[2]; + bool nulls[2] = {false, false}; + + values[0] = Int32GetDatum(segpid->segid); + values[1] = Int32GetDatum(segpid->pid); + tuplestore_putvalues(tupstore, tupdesc, values, nulls); + } + + /* + * QD-only query (INSERT ... VALUES, catalog reads, and other coordinator- + * local plans): no QE gang ran, so backend_info is empty even though the + * coordinator is executing and will push its own per-node batch. Report the + * coordinator itself (segindex -1) so the caller does not mistake an empty + * QE list for a finished query and drop the QD's batch. + */ + if (list_length(backend_info) == 0) + { + Datum values[2]; + bool nulls[2] = {false, false}; + + values[0] = Int32GetDatum(GpIdentity.segindex); + values[1] = Int32GetDatum(proc->pid); + tuplestore_putvalues(tupstore, tupdesc, values, nulls); + } + + return (Datum) 0; +} + +/* + * cbdb_mpp_query_state -- QE-side entry point dispatched by CollectQEQueryState. + * + * Receives an array of gp_segment_pid, filters those belonging to this + * segment, and fires QueryStatePollReason at each matching backend. + */ +PG_FUNCTION_INFO_V1(cbdb_mpp_query_state); +Datum +cbdb_mpp_query_state(PG_FUNCTION_ARGS) +{ + ListCell *iter; + List *alive_procs = get_query_backend_info(PG_GETARG_ARRAYTYPE_P(0)); + bytea *trace_id = PG_GETARG_BYTEA_P(1); + + if (VARSIZE_ANY_EXHDR(trace_id) != GPSC_TRACE_ID_LEN) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid size of trace_id: %zu, expected %d", + VARSIZE_ANY_EXHDR(trace_id), GPSC_TRACE_ID_LEN))); + + if (alive_procs == NIL) + PG_RETURN_NULL(); + + /* Set request parameters for signal handler. */ + params->verbose = true; + params->costs = true; + params->timing = true; + params->buffers = true; + params->triggers = false; + params->format = EXPLAIN_FORMAT_JSON; + + foreach(iter, alive_procs) + { + PGPROC *proc = (PGPROC *) lfirst(iter); + int sig_result; + + if (!proc || proc->backendId == InvalidBackendId) + continue; + + /* Stamp the target's own trace slot before signalling it. */ + memcpy(qs_trace_slots[proc->backendId], VARDATA_ANY(trace_id), + GPSC_TRACE_ID_LEN); + + sig_result = SendProcSignal(proc->pid, QueryStatePollReason, + proc->backendId); + if (sig_result == -1) + ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("cbdb_mpp_query_state: failed to send signal to pid %d", + proc->pid))); + } + PG_RETURN_VOID(); +} + +/* + * get_query_backend_info -- convert a gp_segment_pid[] SQL array to a list + * of PGPROC pointers for backends running on this segment. + * + * Skips entries for other segments and entries whose backend has exited. + */ +static List * +get_query_backend_info(ArrayType *array) +{ + int16 typlen; + bool typbyval; + char typalign; + Oid element_type = ARR_ELEMTYPE(array); + Datum *data; + bool *nulls; + int nitems; + int len; + List *alive_procs = NIL; + + get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); + deconstruct_array(array, element_type, typlen, typbyval, typalign, + &data, &nulls, &nitems); + + len = ArrayGetNItems(ARR_NDIM(array), ARR_DIMS(array)); + + for (int i = 0; i < len; i++) + { + if (nulls[i]) + continue; + + HeapTupleHeader td = DatumGetHeapTupleHeader(data[i]); + TupleDesc tupdesc; + HeapTupleData tmptup; + int32 pid; + int32 segid; + bool segid_isnull = false; + bool pid_isnull = false; + PGPROC *proc; + + tupdesc = lookup_rowtype_tupdesc_copy( + HeapTupleHeaderGetTypeId(td), HeapTupleHeaderGetTypMod(td)); + tmptup.t_len = HeapTupleHeaderGetDatumLength(td); + tmptup.t_data = td; + + segid = DatumGetInt32(heap_getattr(&tmptup, 1, tupdesc, &segid_isnull)); + pid = DatumGetInt32(heap_getattr(&tmptup, 2, tupdesc, &pid_isnull)); + FreeTupleDesc(tupdesc); + + if (segid_isnull || pid_isnull || segid != GpIdentity.segindex) + continue; + + proc = BackendPidGetProc(pid); + if (!proc) + continue; + + alive_procs = lappend(alive_procs, proc); + } + return alive_procs; +} diff --git a/gpcontrib/gp_stats_collector/src/pg_query_state/pg_query_state.h b/gpcontrib/gp_stats_collector/src/pg_query_state/pg_query_state.h new file mode 100644 index 00000000000..d8a837fca65 --- /dev/null +++ b/gpcontrib/gp_stats_collector/src/pg_query_state/pg_query_state.h @@ -0,0 +1,275 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * pg_query_state.h + * Public API for the pg_query_state signal-dispatch layer. + * + * This header is included by both the C extension entry point + * (gp_stats_collector.c) and the C++ hook wrappers (hook_wrappers.cpp). + * Keep it C-compatible: no C++ types, wrapped in extern "C". + * + * Portions derived from pg_query_state + * (https://github.com/postgrespro/pg_query_state), under the PostgreSQL + * License: + * Portions Copyright (c) 2016-2025, Postgres Professional + * + * IDENTIFICATION + * gpcontrib/gp_stats_collector/src/pg_query_state/pg_query_state.h + * + *------------------------------------------------------------------------- + */ +#ifndef __PG_QUERY_STATE_H__ +#define __PG_QUERY_STATE_H__ +#ifdef __cplusplus +extern "C" { +#endif + +#include "postgres.h" + +#include "commands/explain.h" +#include "nodes/pg_list.h" +#include "storage/procarray.h" +#include "storage/shm_mq.h" +#include "cdb/cdbdispatchresult.h" +#include "qs_types.h" + +/* Shared memory queue capacity for passing query-state messages. */ +#define QUEUE_SIZE (64 * 1024) + +/* Maximum single chunk size when splitting a message across shm_mq sends. */ +#define MSG_MAX_SIZE (4 * 1024) + +/* Delay between shm_mq send retries, in microseconds (100 ms). */ +#define WRITING_DELAY (100 * 1000) + +/* Maximum number of send retries before giving up. */ +#define NUM_OF_ATTEMPTS 6 + +/* Bitmask flags for caller-side warnings embedded in shm_mq_msg.warnings. */ +#define TIMING_OFF_WARNING 1 +#define BUFFERS_OFF_WARNING 2 + +/* Unique key that identifies our shm_toc segment. */ +#define PG_QS_MODULE_KEY 0xCA94B108 + +/* Table-of-contents slot indices within the shm_toc segment. */ +#define PG_QS_RCV_KEY 0 +#define PG_QS_SND_KEY 1 + +/* + * Timeouts for shm_mq operations. + * The receive timeout must exceed the send timeout so that waiting workers + * always give up before the polling process stops listening. + */ +#define MAX_RCV_TIMEOUT 2000 /* ms */ +#define MAX_SND_TIMEOUT 1000 /* ms */ + +/* + * Sleep between partial-receive retries (SHM_MQ_WOULD_BLOCK case). + * Must be less than MAX_RCV_TIMEOUT. + */ +#define PART_RCV_DELAY 100 /* ms */ + +/* + * Minimum interval between coordinator plan-doc pushes for the same query. + * SendQueryState() re-sends the ExplainPrintPlan document only after this + * interval elapses, so repeated polls of a long-running query do not resend + * the (unchanging) plan on every signal. + */ +#define PLAN_DOC_RESEND_INTERVAL_MS (2 * 60 * 1000) + +/* + * Status codes returned by the signal handler to describe the state of the + * queried backend. + */ +typedef enum +{ + QUERY_NOT_RUNNING, /* backend is idle or has no active QueryDesc */ + STAT_DISABLED, /* pg_query_state.enable = false */ + QS_RETURNED /* handler successfully collected and sent stats */ +} PG_QS_RequestResult; + +/* + * Wire format for a query-state reply message transmitted through shm_mq. + * The variable-length `stack` field carries sequentially laid out text frames, + * one per stack depth. + */ +typedef struct +{ + int reqid; + int length; /* total message size including flexible array */ + PGPROC *proc; + PG_QS_RequestResult result_code; + int warnings; /* bitmask of TIMING_OFF_WARNING / BUFFERS_OFF_WARNING */ + int stack_depth; + char stack[FLEXIBLE_ARRAY_MEMBER]; +} shm_mq_msg; + +#define BASE_SIZEOF_SHM_MQ_MSG (offsetof(shm_mq_msg, stack_depth)) + +/* + * Compact identifier for a backend running on a specific segment. + */ +typedef struct +{ + int32 segid; + int32 pid; +} gp_segment_pid; + +/* + * Wire format for the backend-info (CDB segment PIDs) reply. + */ +typedef struct +{ + int reqid; + int length; + PGPROC *proc; + PG_QS_RequestResult result_code; + int number; + gp_segment_pid pids[FLEXIBLE_ARRAY_MEMBER]; +} backend_info; + +#define BASE_SIZEOF_GP_BACKEND_INFO (offsetof(backend_info, pids)) + +/* + * Parameters passed through shared memory from the requestor to the signal + * handler, controlling what the handler should collect and how. + */ +typedef struct +{ + ProcSignalReason reason; + int reqid; + bool verbose; + bool costs; + bool timing; + bool buffers; + bool triggers; + ExplainFormat format; +} pg_qs_params; + +/* + * Context threaded through the plan-tree walker. + * per_node_stats accumulates one GpscNodeSample per visited node. + */ +typedef struct QsWalkerContext +{ + List *per_node_stats; + int32_t parent_plan_node_id; + bool finalize; /* true only in pg_qs_executor end */ + TimestampTz ts_now; +} QsWalkerContext; + +/* + * Result code for the chunked shm_mq send helper. + */ +typedef enum +{ + MSG_BY_PARTS_SUCCEEDED, + MSG_BY_PARTS_FAILED +} msg_by_parts_result; + +extern bool pg_qs_enable; +extern bool pg_qs_timing; +extern bool pg_qs_buffers; +extern List *QueryDescStack; +extern pg_qs_params *params; +extern shm_mq *mq; +extern uint32 *mq_req_id; + +/* + * Per-backend trace_id slots, indexed by BackendId (1..MaxBackends; slot 0 for + * InvalidBackendId is unused). The single shared `params` cannot carry the + * trace across an asynchronous ProcSignal: two concurrent collections would + * clobber it and a signaled backend would stamp its batch with the wrong + * trace. The dispatcher writes qs_trace_slots[target->backendId] before + * signalling; the signaled backend reads qs_trace_slots[MyBackendId]. The slot + * is keyed by backend, not by collection, so two overlapping collections of the + * same backend still share one slot -- the caller must not poll one pid twice + * concurrently. + */ +extern char (*qs_trace_slots)[GPSC_TRACE_ID_LEN]; + +extern ProcSignalReason QueryStatePollReason; +extern ProcSignalReason BackendInfoPollReason; + +/* + * pg_qs_init -- register shared memory, custom signals and GUC variables. + * Must be called from _PG_init() during shared_preload_libraries processing. + */ +extern void pg_qs_init(void); + +/* Executor lifecycle hooks -- called from hook_wrappers.cpp. */ +extern void pg_qs_executor_start(QueryDesc *queryDesc, int eflags); +extern void pg_qs_executor_run(QueryDesc *queryDesc); +extern void pg_qs_executor_finish(QueryDesc *queryDesc); +extern void pg_qs_executor_end(QueryDesc *queryDesc); + +/* Shared-memory queue receive helper with millisecond deadline. */ +extern shm_mq_result shm_mq_receive_with_timeout(shm_mq_handle *mqh, + Size *nbytesp, + void **datap, + int64 timeout); + +/* QueryDescStack push/pop helpers. */ +extern void pg_qs_pop_query(void); +extern void pg_qs_push_query(QueryDesc *); + +/* Custom signal handlers registered with RegisterCustomProcSignalHandler. */ +extern void SendQueryState(void); +extern void SendCdbComponents(void); + +/* Shared-memory lock helpers. */ +extern void UnlockShmem(LOCKTAG *tag); +extern void LockShmem(LOCKTAG *tag, uint32 key); + +/* Chunked shm_mq send. */ +extern msg_by_parts_result send_msg_by_parts(shm_mq_handle *mqh, + Size nbytes, + const void *data); + +/* Plan-tree walker and per-node stat collectors. */ +typedef void (*qs_planstate_walker_callback)(PlanState *, QsWalkerContext *); +extern void qs_planstate_walker(PlanState *, qs_planstate_walker_callback, + QsWalkerContext *, int depth); +extern void qs_get_node_stats(PlanState *, QsWalkerContext *); + +/* Debug logging helpers -- emit collected stats to PostgreSQL LOG. */ +extern void qs_debug_node_stats(List *per_node_stats); +extern void qs_debug_node_sample(GpscNodeSample *sample); + +/* + * emit_node_batch -- flatten a List into an array and push + * it to the yagpcc UDS sink as a single SetPerNodeBatchReq (one connection + * per backend). No-op on an empty list. The caller must have invoked + * gpsc_qs_sync_config() first. + */ +extern void emit_node_batch(List *per_node_stats, const char *trace_id); + +/* Query filtering and miscellaneous helpers. */ +extern bool filter_query(QueryDesc *queryDesc); +extern bool wait_for_mq_detached(shm_mq_handle *mqh); +extern bool is_querystack_empty(void); +extern QueryDesc *get_toppest_query(void); + +extern void gpsc_reset_node_roll_state(void); + +#ifdef __cplusplus +} +#endif +#endif /* __PG_QUERY_STATE_H__ */ diff --git a/gpcontrib/gp_stats_collector/src/pg_query_state/qs_types.h b/gpcontrib/gp_stats_collector/src/pg_query_state/qs_types.h new file mode 100644 index 00000000000..ce9ed5287a8 --- /dev/null +++ b/gpcontrib/gp_stats_collector/src/pg_query_state/qs_types.h @@ -0,0 +1,93 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * qs_types.h + * Per-node sample type collected by the pg_query_state plan-tree walker. + * + * IDENTIFICATION + * gpcontrib/gp_stats_collector/src/pg_query_state/qs_types.h + * + *------------------------------------------------------------------------- + */ +#ifndef QS_TYPES_H +#define QS_TYPES_H + +#include +#include + +#define GPSC_TRACE_ID_LEN 16 + +/* + * Execution phase of a single plan node as observed at signal time. + */ +typedef enum QsNodeStatus +{ + QS_NODE_STATUS_UNSPECIFIED = 0, + QS_NODE_STATUS_INITIALIZED = 1, /* instrumentation allocated but not yet started */ + QS_NODE_STATUS_EXECUTING = 2, /* currently inside a tuple-fetch call */ + QS_NODE_STATUS_FINISHED = 3 /* at least one full loop completed */ +} QsNodeStatus; + +typedef struct GpscNodeSample +{ + int32_t tmid; /* transaction/time id (gp_gettmid) */ + int32_t ssid; /* gp_session_id */ + int32_t ccnt; /* gp_command_count */ + int32_t plan_node_id; /* Plan.plan_node_id */ + int32_t parent_plan_node_id; /* plan_node_id of logical parent */ + int32_t node_tag; /* nodeTag(plan) */ + int32_t slice_id; /* currentSliceId */ + int32_t segindex; /* GpIdentity.segindex */ + int32_t pid; /* MyProcPid of the sampled backend */ + int32_t dbid; /* GpIdentity.dbid */ + int32_t relation_oid; /* OID of scanned relation, or 0 */ + double plan_rows; /* optimizer row estimate */ + double ntuples; /* Instrumentation.ntuples */ + double tuplecount; /* Instrumentation.tuplecount (in-progress loop) */ + double nloops; /* Instrumentation.nloops */ + double startup; /* Instrumentation.startup (seconds) */ + double total; /* Instrumentation.total (seconds) */ + double firsttuple; /* Instrumentation.firsttuple (seconds) */ + uint64_t shared_blks_hit; + uint64_t shared_blks_read; + QsNodeStatus node_status; + bool eof; /* Instrumentation.eof: node exhausted for + * the current cycle (last fetch returned no + * tuple). Lets consumers tell a finished + * node from one still actively producing. */ + /* + * Spill, from the GP-specific Instrumentation fields. Reliable once the node + * is finalized; a mid-run snapshot is a lower bound (Sort/HashJoin populate + * these only at eager-free / explain-end). + */ + bool workfile_created; /* Instrumentation.workfileCreated */ + int64_t workmem_used; /* Instrumentation.workmemused (bytes) */ + int64_t workmem_wanted; /* Instrumentation.workmemwanted (bytes); >0 == spilled */ + /* + * Derived rate fields, computed in signal_handler from the per-node rolling + * state (previous ntuples and sample time) rather than read from + * Instrumentation. Zero on the node's first sample. + */ + double ntuples_delta; /* tuples produced since the previous sample */ + double tuples_per_sec; /* ntuples_delta divided by the sample interval */ + double time_since_init_sec; /* seconds since the node's first sample */ + bool stalled; /* executing but produced no new tuples and not at eof */ +} GpscNodeSample; + +#endif /* QS_TYPES_H */ diff --git a/gpcontrib/gp_stats_collector/src/pg_query_state/signal_handler.c b/gpcontrib/gp_stats_collector/src/pg_query_state/signal_handler.c new file mode 100644 index 00000000000..78a240058ef --- /dev/null +++ b/gpcontrib/gp_stats_collector/src/pg_query_state/signal_handler.c @@ -0,0 +1,875 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * signal_handler.c + * Custom signal handlers and plan-tree walker for pg_query_state. + * + * This module implements the three custom ProcSignal handlers registered by + * pg_qs_init(): + * + * SendQueryState() -- fired when QueryStatePollReason is received. + * Walks the active plan tree, collects per-node stats, + * logs them, then pushes the whole snapshot to the + * yagpcc UDS sink (and, on the coordinator, the + * deparsed plan document). + * SendCdbComponents() -- fired when BackendInfoPollReason is received (QD only). + * Sends the list of active QE (segid, pid) pairs. + * + * Also contains: + * qs_planstate_walker() -- recursive plan-tree traversal helper. + * qs_get_node_stats() -- per-node stat collection callback. + * qs_debug_node_stats() -- LOG-level dump of a collected stat list. + * qs_debug_node_sample() -- LOG-level dump of a single GpscNodeSample. + * send_msg_by_parts() -- chunked shm_mq send helper. + * + * Portions derived from pg_query_state + * (https://github.com/postgrespro/pg_query_state), under the PostgreSQL + * License: + * Portions Copyright (c) 2016-2025, Postgres Professional + * + * IDENTIFICATION + * gpcontrib/gp_stats_collector/src/pg_query_state/signal_handler.c + * + *------------------------------------------------------------------------- + */ + +#include + +#include "pg_query_state.h" +#include "PlanNodeEmitter.h" + +#include "cdb/cdbexplain.h" +#include "cdb/cdbutil.h" +#include "cdb/cdbvars.h" +#include "libpq-fe.h" +#include "cdb/cdbconn.h" +#include "commands/explain.h" +#include "executor/executor.h" +#include "miscadmin.h" +#include "nodes/execnodes.h" +#include "nodes/plannodes.h" +#include "pgstat.h" +#include "storage/bufmgr.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/memutils.h" +#include "utils/rel.h" +#include "utils/timestamp.h" +#include "utils/hsearch.h" +#include "libpq/pqmq.h" + +/* + * Identity of the most recent coordinator plan-doc push, used to rate-limit + * SetQueryPlanReq: SendQueryState() re-sends the deparsed plan only when the + * query key changes or PLAN_DOC_RESEND_INTERVAL_MS has elapsed. + */ +static struct +{ + int32_t tmid; + int32_t ssid; + int32_t ccnt; + TimestampTz at; +} last_sent_query_key; + +typedef struct NodeRollState +{ + int32_t plan_node_id; + double prev_ntuples_sum; + TimestampTz prev_executed_at; + TimestampTz first_executed_at; +} NodeRollState; + +static HTAB *node_roll_htab = NULL; + +static void ensure_node_roll_htab(void) +{ + HASHCTL ctl; + + if (node_roll_htab) + { + return; + } + + memset(&ctl, 0, sizeof(ctl)); + ctl.keysize = sizeof(int); + ctl.entrysize = sizeof(NodeRollState); + ctl.hcxt = TopMemoryContext; + node_roll_htab = hash_create("gpsc_per_node_roll_state", + 64, &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +void gpsc_reset_node_roll_state(void) +{ + if (node_roll_htab) + { + hash_destroy(node_roll_htab); + node_roll_htab = NULL; + } +} + +/* + * shm_mq_send_nonblocking -- attempt to send nbytes through mqh up to + * `attempts` times, sleeping WRITING_DELAY µs between retries. + * + * Returns MSG_BY_PARTS_FAILED immediately on SHM_MQ_DETACHED; retries on + * SHM_MQ_WOULD_BLOCK. + */ +static msg_by_parts_result +shm_mq_send_nonblocking(shm_mq_handle *mqh, Size nbytes, + const void *data, Size attempts) +{ + int i; + shm_mq_result res; + + for (i = 0; i < (int) attempts; i++) + { +#if PG_VERSION_NUM < 150000 + res = shm_mq_send(mqh, nbytes, data, true); +#else + res = shm_mq_send(mqh, nbytes, data, true, true); +#endif + + if (res == SHM_MQ_SUCCESS) + break; + else if (res == SHM_MQ_DETACHED) + return MSG_BY_PARTS_FAILED; + + /* SHM_MQ_WOULD_BLOCK -- back off briefly and retry. */ + pg_usleep(WRITING_DELAY); + } + + if (i == (int) attempts) + return MSG_BY_PARTS_FAILED; + + return MSG_BY_PARTS_SUCCEEDED; +} + +/* + * send_msg_by_parts -- transmit an arbitrarily large buffer through mqh. + * + * The wire protocol is: first send a Size value announcing the total payload + * length, then send the payload itself in chunks of at most MSG_MAX_SIZE + * bytes. The receiver must use receive_msg_by_parts() (in pg_query_state.c) + * to reassemble the chunks. + * + * Parameters: + * mqh -- attached shm_mq handle (sender side) + * nbytes -- total payload size + * data -- pointer to the payload + * + * Returns MSG_BY_PARTS_SUCCEEDED on success, MSG_BY_PARTS_FAILED otherwise. + */ +msg_by_parts_result +send_msg_by_parts(shm_mq_handle *mqh, Size nbytes, const void *data) +{ + int offset; + int bytes_left; + int bytes_send; + + /* Announce total length. */ + if (shm_mq_send_nonblocking(mqh, sizeof(Size), &nbytes, + NUM_OF_ATTEMPTS) == MSG_BY_PARTS_FAILED) + return MSG_BY_PARTS_FAILED; + + /* Send payload in chunks. */ + for (offset = 0; offset < (int) nbytes; offset += bytes_send) + { + bytes_left = nbytes - offset; + bytes_send = (bytes_left < MSG_MAX_SIZE) ? bytes_left : MSG_MAX_SIZE; + if (shm_mq_send_nonblocking(mqh, bytes_send, + &(((unsigned char *) data)[offset]), + NUM_OF_ATTEMPTS) == MSG_BY_PARTS_FAILED) + return MSG_BY_PARTS_FAILED; + } + + return MSG_BY_PARTS_SUCCEEDED; +} + +/* + * qs_planstate_walker -- depth-first traversal of a PlanState tree. + * + * Visits every node in the tree rooted at `planstate`, calling `executor` + * on each node before recursing. Handles all node types that have child + * plan states (Append, MergeAppend, BitmapAnd/Or, SubqueryScan, CustomScan, + * init-plans, and sub-plans). + * + * Parameters: + * planstate -- root of the subtree to walk (NULL is a no-op) + * executor -- callback invoked for each node + * qs_walker_ctx -- context threaded through all callbacks + * depth -- current recursion depth (for stack-depth checks) + */ +void +qs_planstate_walker(PlanState *planstate, + qs_planstate_walker_callback executor, + QsWalkerContext *qs_walker_ctx, + int depth) +{ + int32 saved_parent_plan_node_id; + Plan *plan; + ListCell *lc; + + if (planstate == NULL) + return; + + check_stack_depth(); + + plan = planstate->plan; + + executor(planstate, qs_walker_ctx); + saved_parent_plan_node_id = qs_walker_ctx->parent_plan_node_id; + qs_walker_ctx->parent_plan_node_id = plan->plan_node_id; + + /* initPlans */ + foreach(lc, planstate->initPlan) + { + SubPlanState *sps = lfirst_node(SubPlanState, lc); + qs_planstate_walker(sps->planstate, executor, qs_walker_ctx, depth + 1); + } + + /* Left and right children. */ + qs_planstate_walker(outerPlanState(planstate), executor, qs_walker_ctx, + depth + 1); + qs_planstate_walker(innerPlanState(planstate), executor, qs_walker_ctx, + depth + 1); + + /* Type-specific child plans. */ + switch (nodeTag(plan)) + { + case T_Append: + { + AppendState *as = (AppendState *) planstate; + for (int i = 0; i < as->as_nplans; i++) + qs_planstate_walker(as->appendplans[i], executor, + qs_walker_ctx, depth + 1); + break; + } + case T_MergeAppend: + { + MergeAppendState *ms = (MergeAppendState *) planstate; + for (int i = 0; i < ms->ms_nplans; i++) + qs_planstate_walker(ms->mergeplans[i], executor, + qs_walker_ctx, depth + 1); + break; + } + case T_BitmapAnd: + { + BitmapAndState *bas = (BitmapAndState *) planstate; + for (int i = 0; i < bas->nplans; i++) + qs_planstate_walker(bas->bitmapplans[i], executor, + qs_walker_ctx, depth + 1); + break; + } + case T_BitmapOr: + { + BitmapOrState *bos = (BitmapOrState *) planstate; + for (int i = 0; i < bos->nplans; i++) + qs_planstate_walker(bos->bitmapplans[i], executor, + qs_walker_ctx, depth + 1); + break; + } + case T_SubqueryScan: + qs_planstate_walker(((SubqueryScanState *) planstate)->subplan, + executor, qs_walker_ctx, depth + 1); + break; + case T_CustomScan: + foreach(lc, ((CustomScanState *) planstate)->custom_ps) + qs_planstate_walker((PlanState *) lfirst(lc), executor, + qs_walker_ctx, depth + 1); + break; + default: + break; + } + + /* subPlans */ + foreach(lc, planstate->subPlan) + { + SubPlanState *sps = lfirst_node(SubPlanState, lc); + qs_planstate_walker(sps->planstate, executor, qs_walker_ctx, depth + 1); + } + + qs_walker_ctx->parent_plan_node_id = saved_parent_plan_node_id; +} + +/* + * qs_get_node_stats -- walker callback that snapshots one plan node. + * + * Allocates a GpscNodeSample in the current memory context, fills it from + * planstate->instrument (if available), and appends it to + * qs_walker_ctx->per_node_stats. + * + * Parameters: + * planstate -- the plan node being sampled + * qs_walker_ctx -- walker context; per_node_stats is extended in-place + */ +void +qs_get_node_stats(PlanState *planstate, QsWalkerContext *qs_walker_ctx) +{ + GpscNodeSample *nodestat = + (GpscNodeSample *) palloc0(sizeof(GpscNodeSample)); + + /* Identity fields. */ + nodestat->ssid = gp_session_id; + gp_gettmid(&nodestat->tmid); + nodestat->ccnt = gp_command_count; + + /* Plan-tree position. */ + nodestat->plan_node_id = planstate->plan->plan_node_id; + nodestat->parent_plan_node_id = qs_walker_ctx->parent_plan_node_id; + nodestat->node_tag = nodeTag(planstate->plan); + nodestat->slice_id = currentSliceId; + nodestat->segindex = GpIdentity.segindex; + nodestat->dbid = GpIdentity.dbid; + nodestat->pid = MyProcPid; + + /* Planner estimate. */ + nodestat->plan_rows = planstate->plan->plan_rows; + + /* Runtime instrumentation (may be NULL for non-instrumented nodes). */ + if (planstate->instrument) + { + Instrumentation *instr = planstate->instrument; + double eff_nloops; + + if (qs_walker_ctx->finalize) + { + InstrEndLoop(instr); + } + + /* + * Effective number of completed passes. instr->nloops counts only the + * loops closed by InstrEndLoop, which for a top-level node does not fire + * until executor shutdown, so a scan that has already exhausted its + * single pass mid-query still reads 0 -- any nloops-based "done" check + * stays blind to a scan we can otherwise see has ended. instr->eof marks + * that the current pass has finished producing, so fold it in here: this + * surfaces "one pass done" the instant a scan hits eof, and gives a + * rescanning node its in-progress pass too. We must not call + * InstrEndLoop ourselves to force this -- it mutates the live query's + * instrumentation. At finalize InstrEndLoop (above) has already closed + * the loop, so eof must not be counted a second time there. + */ + eff_nloops = instr->nloops; + if (!qs_walker_ctx->finalize && instr->eof) + eff_nloops += 1; + + nodestat->ntuples = instr->ntuples + instr->tuplecount; /* include in-progress loop */ + nodestat->tuplecount = instr->tuplecount; + nodestat->nloops = eff_nloops; + nodestat->startup = instr->startup; + nodestat->total = instr->total; + nodestat->firsttuple = instr->firsttuple; + + nodestat->shared_blks_hit = instr->bufusage.shared_blks_hit; + nodestat->shared_blks_read = instr->bufusage.shared_blks_read; + + /* + * eof lets a consumer tell a node that has finished producing (running + * but exhausted for this cycle) from one still actively pulling. + */ + nodestat->eof = instr->eof; + + /* + * A node that hit eof has finished producing for this cycle even if + * instr->running still reads true between fetches and InstrEndLoop has + * not closed the loop yet -- treat it as done. eff_nloops already folds + * that pass in, so it drives the FINISHED test. + */ + if (instr->running && !instr->eof) + nodestat->node_status = QS_NODE_STATUS_EXECUTING; + else if (eff_nloops > 0) + nodestat->node_status = QS_NODE_STATUS_FINISHED; + else + nodestat->node_status = QS_NODE_STATUS_INITIALIZED; + + /* + * Per-node spill, from the GP-specific Instrumentation fields. These are + * populated by the node executors: workfileCreated live at spill for + * Agg/HashJoin/Hash, at eager-free for Sort; workmemused/workmemwanted at + * batch boundaries (Agg) or explain-end. A running snapshot is therefore a + * lower bound — consumers treat it as such. + */ + nodestat->workfile_created = instr->workfileCreated; + nodestat->workmem_used = (int64_t) instr->workmemused; + nodestat->workmem_wanted = (int64_t) instr->workmemwanted; + } + else + { + nodestat->node_status = QS_NODE_STATUS_INITIALIZED; + } + + /* + * Populate relation_oid for scan nodes by looking up the range-table + * entry using the node's scanrelid. EState.es_range_table is a flat + * List indexed 1-based by scanrelid. + */ + switch (nodeTag(planstate->plan)) + { + case T_SeqScan: + case T_IndexScan: + case T_IndexOnlyScan: + case T_BitmapHeapScan: + case T_TidScan: + { + Index scanrelid = ((Scan *) planstate->plan)->scanrelid; + if (scanrelid > 0 && planstate->state != NULL) + { + List *rtable = planstate->state->es_range_table; + if (scanrelid <= (Index) list_length(rtable)) + { + RangeTblEntry *rte = (RangeTblEntry *) + list_nth(rtable, (int) scanrelid - 1); + if (rte->rtekind == RTE_RELATION) + nodestat->relation_oid = (int32_t) rte->relid; + } + } + break; + } + default: + break; + } + + qs_walker_ctx->per_node_stats = + lappend(qs_walker_ctx->per_node_stats, nodestat); + + { + TimestampTz ts_now = qs_walker_ctx->ts_now; + double cur_sum = nodestat->ntuples; + bool found; + NodeRollState *rs; + + if (!node_roll_htab) + { + ensure_node_roll_htab(); + } + + rs = (NodeRollState *) hash_search(node_roll_htab, + &nodestat->plan_node_id, HASH_ENTER, &found); + + if (found) + { + double dt = (double) (ts_now - rs->prev_executed_at) / USECS_PER_SEC; + nodestat->ntuples_delta = cur_sum - rs->prev_ntuples_sum; + nodestat->tuples_per_sec = (dt > 0) ? nodestat->ntuples_delta / dt : 0; + nodestat->time_since_init_sec = (double) (ts_now - rs->first_executed_at) / USECS_PER_SEC; + } + else + { + nodestat->ntuples_delta = cur_sum; + nodestat->tuples_per_sec = 0; + nodestat->time_since_init_sec = 0; + rs->first_executed_at = ts_now; + } + nodestat->stalled = (nodestat->ntuples_delta == 0 + && nodestat->node_status == QS_NODE_STATUS_EXECUTING + && !nodestat->eof); + rs->prev_ntuples_sum = cur_sum; + rs->prev_executed_at = ts_now; + } +} + +/* + * qs_debug_node_sample -- emit a single GpscNodeSample to the PostgreSQL LOG. + * + * Intended for development and integration testing. In production deployments + * this will produce a large number of log lines; suppress with log_min_messages. + */ +void +qs_debug_node_sample(GpscNodeSample *s) +{ + elog(DEBUG1, + "GpscNodeSample: " + "plan_node_id=%d parent=%d node_tag=%d " + "slice_id=%d segindex=%d " + "tmid=%d ssid=%d ccnt=%d " + "plan_rows=%.0f " + "ntuples=%.0f tuplecount=%.0f nloops=%.0f " + "startup=%f total=%f firsttuple=%f " + "shared_blks_hit=%lu shared_blks_read=%lu " + "workfile_created=%d workmem_used=%ld workmem_wanted=%ld " + "node_status=%d", + s->plan_node_id, s->parent_plan_node_id, s->node_tag, + s->slice_id, s->segindex, + s->tmid, s->ssid, s->ccnt, + s->plan_rows, + s->ntuples, s->tuplecount, s->nloops, + s->startup, s->total, s->firsttuple, + s->shared_blks_hit, s->shared_blks_read, + (int) s->workfile_created, (long) s->workmem_used, (long) s->workmem_wanted, + (int) s->node_status); +} + +/* + * qs_debug_node_stats -- emit all nodes in per_node_stats to the PostgreSQL LOG. + * + * Logs a summary line followed by one line per node via qs_debug_node_sample(). + */ +void +qs_debug_node_stats(List *per_node_stats) +{ + ListCell *lc; + int i = 0; + + elog(DEBUG1, "GpscNodeSample list: %d nodes", list_length(per_node_stats)); + foreach(lc, per_node_stats) + { + GpscNodeSample *s = (GpscNodeSample *) lfirst(lc); + elog(DEBUG1, "--- node[%d] ---", i++); + qs_debug_node_sample(s); + } +} + +/* + * runtime_explain -- snapshot the active query's plan tree. + * + * Retrieves the top-most QueryDesc from QueryDescStack, walks its planstate + * tree with qs_get_node_stats(), and returns the resulting List of + * GpscNodeSample pointers. + * + * Callers must ensure QueryDescStack is non-empty before calling this. + */ +static List * +runtime_explain(void) +{ + QsWalkerContext *qs_walker_ctx = + (QsWalkerContext *) palloc0(sizeof(QsWalkerContext)); + QueryDesc *queryDesc; + + Assert(list_length(QueryDescStack) > 0); + queryDesc = get_toppest_query(); + qs_walker_ctx->ts_now = GetCurrentTimestamp(); + qs_planstate_walker(queryDesc->planstate, qs_get_node_stats, + qs_walker_ctx, 0); + return qs_walker_ctx->per_node_stats; +} + +/* + * emit_node_batch -- push a whole plan-tree snapshot as one SetPerNodeBatchReq. + * + * Flattens the List into a contiguous array and hands it to + * the C++ emitter, which opens a single UDS connection for the whole backend + * instead of one connection per node. A NULL or empty list is a no-op. + * + * The caller is responsible for calling gpsc_qs_sync_config() beforehand. + */ +void +emit_node_batch(List *per_node_stats, const char *trace_id) +{ + GpscNodeSample **arr; + ListCell *lc; + int n = list_length(per_node_stats); + int i = 0; + + if (n == 0) + return; + + arr = (GpscNodeSample **) palloc(n * sizeof(GpscNodeSample *)); + foreach(lc, per_node_stats) + arr[i++] = (GpscNodeSample *) lfirst(lc); + + gpsc_emit_node_batch(arr, n, trace_id); +} + +/* + * build_plan_doc -- render the active query's plan via ExplainPrintPlan. + * + * Produces the full deparsed plan document (expressions, costs, Settings) in + * the requested ExplainFormat. ExplainBeginOutput/ExplainEndOutput and the + * enclosing "Query" group frame the output so JSON/XML/YAML come out + * well-formed: ExplainPrintPlan on its own renders only the inner "Plan" + * property, so without the group the non-text formats are an unwrapped + * fragment no parser accepts. The framing lives here, outside + * ExplainPrintPlan, so that function is left untouched. + * + * Returns a palloc'd string in the current context, or NULL when queryDesc is + * NULL. Intended for the coordinator (QD) only: on a QE the plan subtree can + * reach child PlanStates from other slices that are not instantiated here. + */ +static char * +build_plan_doc(QueryDesc *queryDesc, ExplainFormat format) +{ + ExplainState *es; + + if (queryDesc == NULL) + return NULL; + + HOLD_INTERRUPTS(); + { + es = NewExplainState(); + es->format = format; + es->verbose = true; + es->costs = true; + es->runtime = true; + ExplainBeginOutput(es); + ExplainOpenGroup("Query", NULL, true, es); + ExplainPrintPlan(es, queryDesc); + ExplainCloseGroup("Query", NULL, true, es); + ExplainEndOutput(es); + } + RESUME_INTERRUPTS(); + + return es->str->data; +} + +/* + * SendQueryState -- handler for QueryStatePollReason. + * + * Fired asynchronously when another backend (or the monitoring function) + * sends QueryStatePollReason to this process. + * + * Collects a plan-tree snapshot via runtime_explain(), logs it via + * qs_debug_node_stats(), then syncs the emitter config and pushes the whole + * snapshot to the yagpcc UDS sink via emit_node_batch(). On the coordinator + * it additionally pushes the deparsed plan document (SetQueryPlanReq), which + * the compact per-node stats cannot reconstruct; that push is rate-limited to + * once per PLAN_DOC_RESEND_INTERVAL_MS per query. + * + * The entire body runs inside a dedicated MemoryContext that is deleted on + * exit, preventing any leaks into the backend's long-lived contexts. Any + * errors are swallowed with FlushErrorState() to avoid crashing the backend. + */ +void +SendQueryState(void) +{ + MemoryContext oldcontext; + MemoryContext qs_context; + List *qs_result = NIL; + + if (!pg_qs_enable) + return; /* STAT_DISABLED */ + + if (!list_length(QueryDescStack)) + return; /* QUERY_NOT_RUNNING */ + + if (stack_is_too_deep()) + { + elog(DEBUG1, "pg_query_state: skipping poll, call stack too deep"); + return; + } + + qs_context = AllocSetContextCreate(TopMemoryContext, + "pg_query_state signal context", + ALLOCSET_DEFAULT_SIZES); + oldcontext = MemoryContextSwitchTo(qs_context); + + PG_TRY(); + { + qs_result = runtime_explain(); + qs_debug_node_stats(qs_result); + + gpsc_qs_sync_config(); + + /* + * Emit the whole plan-tree snapshot as a single batch: one UDS + * connection per backend instead of connect+send+close per node. Key it + * under this backend's own trace slot, stamped by the dispatcher before + * the signal. The slot is per-backend, so distinct backends never + * collide; two overlapping collections of the *same* backend still share + * one slot and can race, so the caller must not poll one pid twice + * concurrently. + */ + emit_node_batch(qs_result, qs_trace_slots[MyBackendId]); + + /* + * Coordinator-only: push the full ExplainPrintPlan document so yagpcc + * has the deparsed structure (expressions, costs, Settings) that the + * compact per-node stats cannot reconstruct. On a QE the plan subtree + * may reach child PlanStates from other slices that are not + * instantiated here, so restrict this to the QD. Rate-limited to once + * per PLAN_DOC_RESEND_INTERVAL_MS per query so repeated polls of a + * long-running query do not resend the unchanging plan every time. + */ + if (Gp_role == GP_ROLE_DISPATCH) + { + bool is_same_query; + bool is_stale; + int32_t tmid; + + gp_gettmid(&tmid); + is_same_query = (tmid == last_sent_query_key.tmid && + gp_session_id == last_sent_query_key.ssid && + gp_command_count == last_sent_query_key.ccnt); + + is_stale = !is_same_query || + TimestampDifferenceExceeds(last_sent_query_key.at, + GetCurrentTimestamp(), + PLAN_DOC_RESEND_INTERVAL_MS); + + if (is_stale) + { + char *plan_doc = build_plan_doc(get_toppest_query(), + EXPLAIN_FORMAT_JSON); + + gpsc_emit_query_plan(tmid, gp_session_id, gp_command_count, + plan_doc, EXPLAIN_FORMAT_JSON); + + last_sent_query_key.tmid = tmid; + last_sent_query_key.ssid = gp_session_id; + last_sent_query_key.ccnt = gp_command_count; + last_sent_query_key.at = GetCurrentTimestamp(); + } + } + } + PG_CATCH(); + { + FlushErrorState(); + } + PG_END_TRY(); + + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(qs_context); +} + +/* + * fill_segpid -- populate consecutive gp_segment_pid slots from one CDB segment. + * + * Iterates the activelist of segInfo and fills msg->pids starting at *index, + * incrementing *index for each entry. + */ +static void +fill_segpid(CdbComponentDatabaseInfo *segInfo, backend_info *msg, int *index) +{ + ListCell *lc; + + foreach(lc, segInfo->activelist) + { + SegmentDatabaseDescriptor *dbdesc = + (SegmentDatabaseDescriptor *) lfirst(lc); + gp_segment_pid *segpid = &msg->pids[(*index)++]; + segpid->pid = dbdesc->backendPid; + segpid->segid = dbdesc->segindex; + } +} + +/* + * SendCdbComponents -- handler for BackendInfoPollReason (QD only). + * + * Collects the list of active QE (segid, pid) pairs from the CDB component + * database and sends them back to the requestor through shm_mq as a + * backend_info message. + * + * Side effects: + * - Calls cdbcomponent_getCdbComponents(), which may allocate memory. + * - All allocations are in a short-lived MemoryContext deleted on exit. + */ +void +SendCdbComponents(void) +{ + shm_mq_handle *mqh = NULL; + CdbComponentDatabases *cdbs; + msg_by_parts_result send_result; + MemoryContext oldctx; + int index = 0; + MemoryContext query_state_ctx = + AllocSetContextCreate(TopMemoryContext, + "pg_query_state SendCdbComponents", + ALLOCSET_DEFAULT_SIZES); + + HOLD_INTERRUPTS(); + oldctx = MemoryContextSwitchTo(query_state_ctx); + + PG_TRY(); + { + mqh = shm_mq_attach(mq, NULL, NULL); + + if (shm_mq_get_sender(mq) != MyProc || + params->reason != BackendInfoPollReason) + { + elog(DEBUG1, "pg_query_state: SendCdbComponents: stale request, discarding"); + shm_mq_detach(mqh); + } + else if (!pg_qs_enable) + { + elog(DEBUG1, "pg_query_state: SendCdbComponents: module disabled"); + shm_mq_msg disabled_msg = {*mq_req_id, BASE_SIZEOF_SHM_MQ_MSG, + MyProc, STAT_DISABLED}; + if (send_msg_by_parts(mqh, disabled_msg.length, + &disabled_msg) != MSG_BY_PARTS_SUCCEEDED) + shm_mq_detach(mqh); + } + else if (list_length(QueryDescStack) == 0) + { + elog(DEBUG1, "pg_query_state: SendCdbComponents: no active query"); + shm_mq_msg not_running_msg = {*mq_req_id, BASE_SIZEOF_SHM_MQ_MSG, + MyProc, QUERY_NOT_RUNNING}; + if (send_msg_by_parts(mqh, not_running_msg.length, + ¬_running_msg) != MSG_BY_PARTS_SUCCEEDED) + shm_mq_detach(mqh); + } + else + { + cdbs = cdbcomponent_getCdbComponents(); + + /* + * Size the buffer by the number of descriptors we will actually + * emit -- the total length of every segment's activelist, which is + * exactly what fill_segpid walks. cdbs->numActiveQEs is NOT that + * count for every plan shape (a coordinator-heavy INSERT ... SELECT + * leaves the activelists and numActiveQEs out of step): sizing by + * numActiveQEs while filling by activelist overran the allocation + * and produced a length the receiver rejected with "unexpected + * message length". Deriving both length and ->number from the same + * walk keeps them consistent and the write in bounds. + */ + int qecount = 0; + for (int i = 0; i < cdbs->total_segment_dbs; i++) + qecount += list_length(cdbs->segment_db_info[i].activelist); + + int msglen = BASE_SIZEOF_GP_BACKEND_INFO + + sizeof(gp_segment_pid) * qecount; + backend_info *msg = (backend_info *) palloc0(msglen); + + msg->reqid = *mq_req_id; + msg->length = msglen; + msg->result_code = QS_RETURNED; + + for (int i = 0; i < cdbs->total_segment_dbs; i++) + { + CdbComponentDatabaseInfo *segInfo = + &cdbs->segment_db_info[i]; + fill_segpid(segInfo, msg, &index); + } + Assert(index == qecount); + msg->number = index; + + send_result = send_msg_by_parts(mqh, msglen, msg); + if (send_result != MSG_BY_PARTS_SUCCEEDED) + shm_mq_detach(mqh); + } + } + PG_CATCH(); + { + elog(WARNING, "pg_query_state: SendCdbComponents: error during send"); + if (!elog_dismiss(WARNING)) + { + if (mqh) + shm_mq_detach(mqh); + + MemoryContextSwitchTo(oldctx); + MemoryContextDelete(query_state_ctx); + RESUME_INTERRUPTS(); + PG_RE_THROW(); + } + } + PG_END_TRY(); + + MemoryContextSwitchTo(oldctx); + MemoryContextDelete(query_state_ctx); + RESUME_INTERRUPTS(); +} diff --git a/gpcontrib/gp_stats_collector/test/Makefile b/gpcontrib/gp_stats_collector/test/Makefile new file mode 100644 index 00000000000..3931f4d9592 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/Makefile @@ -0,0 +1,22 @@ +# Regression tests for the gp_stats_collector pg_query_state signal API. +# +# Self-contained installcheck suite: the extension must already be built and +# installed (make -C .. install) and loaded via shared_preload_libraries in the +# target cluster. Run with: +# +# make -C gpcontrib/gp_stats_collector/test installcheck +# +# pg_regress defaults to ./sql/.sql and ./expected/.out. + +REGRESS = gpsc_pg_query_state + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = gpcontrib/gp_stats_collector/test +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/gpcontrib/gp_stats_collector/test/expected/gpsc_pg_query_state.out b/gpcontrib/gp_stats_collector/test/expected/gpsc_pg_query_state.out new file mode 100644 index 00000000000..c3eb3e535d9 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/expected/gpsc_pg_query_state.out @@ -0,0 +1,57 @@ +-- pg_query_state signal API (extension v1.2): catalog contract + negative paths. +-- +-- Deterministic coverage only: SQL-visible function/type registration and the +-- input-validation error branches. The asynchronous happy path (poll a live +-- query and observe per-node stats) is exercised separately under isolation2, +-- since it depends on a second running backend and timing. +-- start_ignore +CREATE EXTENSION IF NOT EXISTS gp_stats_collector; +-- end_ignore +-- +-- Catalog contract: the three SQL-visible functions are registered in the gpsc +-- schema with the expected return type and dispatch (exec) location. +-- proexeclocation: c = coordinator, a = any (QE-local), s = all segments. +-- +SELECT proname, + pronargs, + prorettype::regtype AS returns, + proexeclocation +FROM pg_proc +WHERE pronamespace = 'gpsc'::regnamespace + AND proname IN ('pg_query_state', 'pg_query_state_backends', 'cbdb_mpp_query_state') +ORDER BY proname; + proname | pronargs | returns | proexeclocation +-------------------------+----------+---------+----------------- + cbdb_mpp_query_state | 2 | void | a + pg_query_state | 2 | void | c + pg_query_state_backends | 1 | record | c +(3 rows) + +-- Composite identifier type used by the signal layer is present. +SELECT typname +FROM pg_type +WHERE typnamespace = 'gpsc'::regnamespace + AND typname = 'gp_segment_pid'; + typname +---------------- + gp_segment_pid +(1 row) + +-- +-- Negative: a backend cannot poll its own state. +-- +SELECT gpsc.pg_query_state(pg_backend_pid(), '\x00112233445566778899aabbccddeeff'::bytea); +ERROR: cannot extract state of current process +SELECT * FROM gpsc.pg_query_state_backends(pg_backend_pid()); +ERROR: cannot extract state of current process +-- +-- Negative: a pid that maps to no live backend is rejected. +-- +SELECT gpsc.pg_query_state(-1, '\x00112233445566778899aabbccddeeff'::bytea); +ERROR: backend with pid=-1 not found +SELECT * FROM gpsc.pg_query_state_backends(-1); +ERROR: backend with pid=-1 not found +-- Cleanup +-- start_ignore +DROP EXTENSION gp_stats_collector; +-- end_ignore diff --git a/gpcontrib/gp_stats_collector/test/isolation2/.gitignore b/gpcontrib/gp_stats_collector/test/isolation2/.gitignore new file mode 100644 index 00000000000..0d2848e26fb --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/.gitignore @@ -0,0 +1,4 @@ +/sql_isolation_testcase.py +/results/ +/regression.diffs +/regression.out diff --git a/gpcontrib/gp_stats_collector/test/isolation2/Makefile b/gpcontrib/gp_stats_collector/test/isolation2/Makefile new file mode 100644 index 00000000000..2a835922c65 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/Makefile @@ -0,0 +1,33 @@ +# isolation2 suite for the gp_stats_collector pg_query_state signal API. +# +# Multi-session tests that a plain pg_regress run cannot express (one backend +# polling another). Reuses the core pg_isolation2_regress harness rather than +# rebuilding it. +# +# Prerequisites (handled by the CI isolation2 job): +# - gp_inject_fault available (--enable-faultinjector, on by default) for the +# happy-path spec. +# - Extension installed and gp_stats_collector in shared_preload_libraries. +# - Harness built: make -C $(top_builddir)/src/test/isolation2 install +# +# Run: +# make -C gpcontrib/gp_stats_collector/test/isolation2 installcheck + +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global + +ISO2 = $(top_builddir)/src/test/isolation2 + +# isolation2_main.c hardcodes "python3 ./sql_isolation_testcase.py", resolved +# from the current directory, so symlink the core driver here before running. +installcheck: + @ln -sf $(ISO2)/sql_isolation_testcase.py ./sql_isolation_testcase.py + $(ISO2)/pg_isolation2_regress \ + --init-file=$(top_builddir)/src/test/regress/init_file \ + --init-file=$(ISO2)/init_file_isolation2 \ + --inputdir=. --outputdir=. \ + --bindir='$(bindir)' \ + --schedule=./isolation2_schedule + +clean: + rm -rf results/ regression.diffs regression.out sql_isolation_testcase.py diff --git a/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_backends.out b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_backends.out new file mode 100644 index 00000000000..a19477a411b --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_backends.out @@ -0,0 +1,27 @@ +-- pg_query_state_backends against an *idle* backend returns an empty set. +-- +-- Deterministic multi-session check with no async race: session 1 tags itself +-- and sits idle; session 2 looks up its pid and polls it. An idle backend is +-- "not running a query", so GetRemoteBackendInfo returns QUERY_NOT_RUNNING and +-- the function yields an empty set (not an error). +-- +-- Extensions are created by setup.sql. + +-- Session 1: tag connection so session 2 can find its pid, then go idle. +1: SET application_name TO 'qs_idle_target'; +SET +1: SELECT 1; + ?column? +---------- + 1 +(1 row) + +-- Session 2: idle target -> zero participating backends. +2: SELECT count(*) AS n_backends FROM gpsc.pg_query_state_backends( (SELECT pid FROM pg_stat_activity WHERE application_name = 'qs_idle_target' AND pid <> pg_backend_pid() ORDER BY backend_start LIMIT 1)); + n_backends +------------ + 0 +(1 row) + +1q: ... +2q: ... diff --git a/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_disabled.out b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_disabled.out new file mode 100644 index 00000000000..792610bb011 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_disabled.out @@ -0,0 +1,61 @@ +-- STAT_DISABLED: when the target backend has pg_query_state.enable = off, its +-- SendCdbComponents reply is STAT_DISABLED, so polling reports an empty backend +-- list even though a query is actively running on the segments. +-- +-- Distinguishes "disabled" from "idle": here the query really is executing +-- (suspended on a fault), yet the disabled module yields nothing. +-- +-- Extensions come from setup.sql. + +CREATE TABLE qs_disabled_t (id int) DISTRIBUTED BY (id); +CREATE +INSERT INTO qs_disabled_t SELECT generate_series(1, 100); +INSERT 100 + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'suspend', dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) + +-- Target disables the module for its own session, then runs a query that hangs. +1: SET application_name TO 'qs_disabled_target'; +SET +1: SET pg_query_state.enable TO off; +SET +1&: SELECT count(*) FROM qs_disabled_t; + +SELECT gp_wait_until_triggered_fault('executor_pre_tuple_processed', 1, dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_wait_until_triggered_fault +------------------------------- + Success: + Success: + Success: +(3 rows) + +-- Running, but module disabled on the target -> empty backend list. +2: SELECT count(*) AS n_backends FROM gpsc.pg_query_state_backends( (SELECT pid FROM pg_stat_activity WHERE application_name = 'qs_disabled_target' AND pid <> pg_backend_pid() ORDER BY backend_start LIMIT 1)); + n_backends +------------ + 0 +(1 row) + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'reset', dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) +1<: <... completed> + count +------- + 100 +(1 row) +1q: ... +2q: ... + +DROP TABLE qs_disabled_t; +DROP diff --git a/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_perms.out b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_perms.out new file mode 100644 index 00000000000..8e8f24e6a41 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_perms.out @@ -0,0 +1,92 @@ +-- Permission gate: a non-superuser that does not own the target query is +-- denied; a superuser is allowed. +-- +-- The gate is superuser() || GetUserId() == proc->roleId. isolation2 runs all +-- sessions under the same session role (the one that launched the harness), so +-- the "non-super owner is allowed" branch cannot be expressed here and is not +-- covered; the deny and superuser-allow branches are. +-- +-- A non-superuser cannot see another backend's application_name in +-- pg_stat_activity, so the target pid is captured (as superuser) into a table +-- before SET ROLE. +-- +-- Extensions come from setup.sql. + +CREATE TABLE qs_perm_t (id int) DISTRIBUTED BY (id); +CREATE +INSERT INTO qs_perm_t SELECT generate_series(1, 100); +INSERT 100 +CREATE TABLE qs_perm_pid (pid int); +CREATE +CREATE ROLE qs_unpriv; +CREATE +GRANT SELECT ON qs_perm_pid TO qs_unpriv; +GRANT +-- No gpsc grants here on purpose: the extension grants USAGE/EXECUTE to PUBLIC +-- in its migration, so an ordinary role reaches the roleId gate exactly as it +-- would in production. This test verifies the gate, not the schema grants. + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'suspend', dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) + +1: SET application_name TO 'qs_perm_target'; +SET +1&: SELECT count(*) FROM qs_perm_t; + +SELECT gp_wait_until_triggered_fault('executor_pre_tuple_processed', 1, dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_wait_until_triggered_fault +------------------------------- + Success: + Success: + Success: +(3 rows) + +-- Capture the target pid as superuser (sees application_name). +2: INSERT INTO qs_perm_pid SELECT pid FROM pg_stat_activity WHERE application_name = 'qs_perm_target' AND pid <> pg_backend_pid() ORDER BY backend_start LIMIT 1; +INSERT 1 + +-- Non-superuser, non-owner: both entry points are denied. +2: SET ROLE qs_unpriv; +SET +2: SELECT gpsc.pg_query_state((SELECT pid FROM qs_perm_pid), '\x00112233445566778899aabbccddeeff'::bytea); +ERROR: permission denied +2: SELECT * FROM gpsc.pg_query_state_backends((SELECT pid FROM qs_perm_pid)); +ERROR: permission denied +2: RESET ROLE; +RESET + +-- Superuser: allowed (non-empty backend list). +2: SELECT count(*) > 0 AS has_backends FROM gpsc.pg_query_state_backends((SELECT pid FROM qs_perm_pid)); + has_backends +-------------- + t +(1 row) + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'reset', dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) +1<: <... completed> + count +------- + 100 +(1 row) +1q: ... +2q: ... + +DROP OWNED BY qs_unpriv; +DROP +DROP ROLE qs_unpriv; +DROP +DROP TABLE qs_perm_pid; +DROP +DROP TABLE qs_perm_t; +DROP diff --git a/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_running.out b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_running.out new file mode 100644 index 00000000000..cbced86b14d --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_running.out @@ -0,0 +1,69 @@ +-- Happy path: a query suspended mid-execution on the QEs is observed live. +-- +-- Session 1 launches a query that hits an 'executor_pre_tuple_processed' +-- suspend fault on every primary segment, so its QE backends sit inside the +-- executor with a live plan tree. Session 2 then: +-- * pg_query_state_backends(pid) -> at least one participating backend, +-- * pg_query_state(pid) -> succeeds (fire-and-forget, returns void). +-- The fault is reset and the suspended query is reaped. +-- +-- Extensions (gp_stats_collector, gp_inject_fault) come from setup.sql. + +CREATE TABLE qs_running_t (id int) DISTRIBUTED BY (id); +CREATE +INSERT INTO qs_running_t SELECT generate_series(1, 100); +INSERT 100 + +-- Suspend execution on all primary segments. +SELECT gp_inject_fault('executor_pre_tuple_processed', 'suspend', dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) + +-- Session 1: tag the connection, then launch a query that hangs on the QEs. +1: SET application_name TO 'qs_running_target'; +SET +1&: SELECT count(*) FROM qs_running_t; + +-- Wait until the fault has been hit on the segments. +SELECT gp_wait_until_triggered_fault('executor_pre_tuple_processed', 1, dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_wait_until_triggered_fault +------------------------------- + Success: + Success: + Success: +(3 rows) + +-- Session 2: the running query has live QE backends, and polling succeeds. +2: SELECT count(*) > 0 AS has_backends FROM gpsc.pg_query_state_backends( (SELECT pid FROM pg_stat_activity WHERE application_name = 'qs_running_target' AND pid <> pg_backend_pid() ORDER BY backend_start LIMIT 1)); + has_backends +-------------- + t +(1 row) +2: SELECT gpsc.pg_query_state( (SELECT pid FROM pg_stat_activity WHERE application_name = 'qs_running_target' AND pid <> pg_backend_pid() ORDER BY backend_start LIMIT 1), '\x00112233445566778899aabbccddeeff'::bytea); + pg_query_state +---------------- + +(1 row) + +-- Release the fault and reap the suspended query. +SELECT gp_inject_fault('executor_pre_tuple_processed', 'reset', dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) +1<: <... completed> + count +------- + 100 +(1 row) +1q: ... +2q: ... + +DROP TABLE qs_running_t; +DROP diff --git a/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_seg_count.out b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_seg_count.out new file mode 100644 index 00000000000..b4ad8a9a84d --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/expected/gpsc_pqs_seg_count.out @@ -0,0 +1,58 @@ +-- backends reports exactly one participating backend per primary segment for a +-- single-gang query -- a strict count rather than the has_backends>0 smoke +-- check in gpsc_pqs_running. +-- +-- A plain scan+count is one gang, so the QE list must match the number of +-- primary segments. +-- +-- Extensions come from setup.sql. + +CREATE TABLE qs_segcount_t (id int) DISTRIBUTED BY (id); +CREATE +INSERT INTO qs_segcount_t SELECT generate_series(1, 100); +INSERT 100 + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'suspend', dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) + +1: SET application_name TO 'qs_segcount_target'; +SET +1&: SELECT count(*) FROM qs_segcount_t; + +SELECT gp_wait_until_triggered_fault('executor_pre_tuple_processed', 1, dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_wait_until_triggered_fault +------------------------------- + Success: + Success: + Success: +(3 rows) + +-- One backend per primary segment. +2: SELECT count(*) = (SELECT count(*) FROM gp_segment_configuration WHERE role = 'p' AND content > -1) AS matches_primaries FROM gpsc.pg_query_state_backends( (SELECT pid FROM pg_stat_activity WHERE application_name = 'qs_segcount_target' AND pid <> pg_backend_pid() ORDER BY backend_start LIMIT 1)); + matches_primaries +------------------- + t +(1 row) + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'reset', dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) +1<: <... completed> + count +------- + 100 +(1 row) +1q: ... +2q: ... + +DROP TABLE qs_segcount_t; +DROP diff --git a/gpcontrib/gp_stats_collector/test/isolation2/expected/setup.out b/gpcontrib/gp_stats_collector/test/isolation2/expected/setup.out new file mode 100644 index 00000000000..f7ab4e44725 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/expected/setup.out @@ -0,0 +1,6 @@ +-- Shared setup for the pg_query_state isolation2 suite. +-- pg_isolation2_regress always runs a "setup" test before the schedule. +CREATE EXTENSION IF NOT EXISTS gp_stats_collector; +CREATE +CREATE EXTENSION IF NOT EXISTS gp_inject_fault; +CREATE diff --git a/gpcontrib/gp_stats_collector/test/isolation2/isolation2_schedule b/gpcontrib/gp_stats_collector/test/isolation2/isolation2_schedule new file mode 100644 index 00000000000..5adb0d9cc0a --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/isolation2_schedule @@ -0,0 +1,20 @@ +# pg_query_state isolation2 schedule. +# +# gpsc_pqs_backends -- deterministic: an idle backend yields an empty backend +# list (no fault injector required). +# gpsc_pqs_running -- happy path: a query suspended mid-execution on the QEs +# is observed via pg_query_state_backends/pg_query_state. +# gpsc_pqs_perms -- permission gate: non-superuser non-owner is denied, +# superuser is allowed. +# gpsc_pqs_disabled -- STAT_DISABLED: target with pg_query_state.enable=off +# reports no backends despite a running query. +# gpsc_pqs_seg_count -- strict count: one participating backend per primary +# segment for a single-gang query. +# +# The gpsc_pqs_* specs after gpsc_pqs_backends use gp_inject_fault (enabled by +# default) to suspend a running query while it is polled. +test: gpsc_pqs_backends +test: gpsc_pqs_running +test: gpsc_pqs_perms +test: gpsc_pqs_disabled +test: gpsc_pqs_seg_count diff --git a/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_backends.sql b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_backends.sql new file mode 100644 index 00000000000..4df90380c7a --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_backends.sql @@ -0,0 +1,21 @@ +-- pg_query_state_backends against an *idle* backend returns an empty set. +-- +-- Deterministic multi-session check with no async race: session 1 tags itself +-- and sits idle; session 2 looks up its pid and polls it. An idle backend is +-- "not running a query", so GetRemoteBackendInfo returns QUERY_NOT_RUNNING and +-- the function yields an empty set (not an error). +-- +-- Extensions are created by setup.sql. + +-- Session 1: tag connection so session 2 can find its pid, then go idle. +1: SET application_name TO 'qs_idle_target'; +1: SELECT 1; + +-- Session 2: idle target -> zero participating backends. +2: SELECT count(*) AS n_backends FROM gpsc.pg_query_state_backends( + (SELECT pid FROM pg_stat_activity + WHERE application_name = 'qs_idle_target' AND pid <> pg_backend_pid() + ORDER BY backend_start LIMIT 1)); + +1q: +2q: diff --git a/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_disabled.sql b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_disabled.sql new file mode 100644 index 00000000000..10bf81d188a --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_disabled.sql @@ -0,0 +1,36 @@ +-- STAT_DISABLED: when the target backend has pg_query_state.enable = off, its +-- SendCdbComponents reply is STAT_DISABLED, so polling reports an empty backend +-- list even though a query is actively running on the segments. +-- +-- Distinguishes "disabled" from "idle": here the query really is executing +-- (suspended on a fault), yet the disabled module yields nothing. +-- +-- Extensions come from setup.sql. + +CREATE TABLE qs_disabled_t (id int) DISTRIBUTED BY (id); +INSERT INTO qs_disabled_t SELECT generate_series(1, 100); + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'suspend', dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + +-- Target disables the module for its own session, then runs a query that hangs. +1: SET application_name TO 'qs_disabled_target'; +1: SET pg_query_state.enable TO off; +1&: SELECT count(*) FROM qs_disabled_t; + +SELECT gp_wait_until_triggered_fault('executor_pre_tuple_processed', 1, dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + +-- Running, but module disabled on the target -> empty backend list. +2: SELECT count(*) AS n_backends FROM gpsc.pg_query_state_backends( + (SELECT pid FROM pg_stat_activity + WHERE application_name = 'qs_disabled_target' AND pid <> pg_backend_pid() + ORDER BY backend_start LIMIT 1)); + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'reset', dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; +1<: +1q: +2q: + +DROP TABLE qs_disabled_t; diff --git a/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_perms.sql b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_perms.sql new file mode 100644 index 00000000000..eadd5c60b33 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_perms.sql @@ -0,0 +1,57 @@ +-- Permission gate: a non-superuser that does not own the target query is +-- denied; a superuser is allowed. +-- +-- The gate is superuser() || GetUserId() == proc->roleId. isolation2 runs all +-- sessions under the same session role (the one that launched the harness), so +-- the "non-super owner is allowed" branch cannot be expressed here and is not +-- covered; the deny and superuser-allow branches are. +-- +-- A non-superuser cannot see another backend's application_name in +-- pg_stat_activity, so the target pid is captured (as superuser) into a table +-- before SET ROLE. +-- +-- Extensions come from setup.sql. + +CREATE TABLE qs_perm_t (id int) DISTRIBUTED BY (id); +INSERT INTO qs_perm_t SELECT generate_series(1, 100); +CREATE TABLE qs_perm_pid (pid int); +CREATE ROLE qs_unpriv; +GRANT SELECT ON qs_perm_pid TO qs_unpriv; +-- No gpsc grants here on purpose: the extension grants USAGE/EXECUTE to PUBLIC +-- in its migration, so an ordinary role reaches the roleId gate exactly as it +-- would in production. This test verifies the gate, not the schema grants. + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'suspend', dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + +1: SET application_name TO 'qs_perm_target'; +1&: SELECT count(*) FROM qs_perm_t; + +SELECT gp_wait_until_triggered_fault('executor_pre_tuple_processed', 1, dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + +-- Capture the target pid as superuser (sees application_name). +2: INSERT INTO qs_perm_pid SELECT pid FROM pg_stat_activity + WHERE application_name = 'qs_perm_target' AND pid <> pg_backend_pid() + ORDER BY backend_start LIMIT 1; + +-- Non-superuser, non-owner: both entry points are denied. +2: SET ROLE qs_unpriv; +2: SELECT gpsc.pg_query_state((SELECT pid FROM qs_perm_pid), '\x00112233445566778899aabbccddeeff'::bytea); +2: SELECT * FROM gpsc.pg_query_state_backends((SELECT pid FROM qs_perm_pid)); +2: RESET ROLE; + +-- Superuser: allowed (non-empty backend list). +2: SELECT count(*) > 0 AS has_backends + FROM gpsc.pg_query_state_backends((SELECT pid FROM qs_perm_pid)); + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'reset', dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; +1<: +1q: +2q: + +DROP OWNED BY qs_unpriv; +DROP ROLE qs_unpriv; +DROP TABLE qs_perm_pid; +DROP TABLE qs_perm_t; diff --git a/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_running.sql b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_running.sql new file mode 100644 index 00000000000..3c8386cc247 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_running.sql @@ -0,0 +1,45 @@ +-- Happy path: a query suspended mid-execution on the QEs is observed live. +-- +-- Session 1 launches a query that hits an 'executor_pre_tuple_processed' +-- suspend fault on every primary segment, so its QE backends sit inside the +-- executor with a live plan tree. Session 2 then: +-- * pg_query_state_backends(pid) -> at least one participating backend, +-- * pg_query_state(pid) -> succeeds (fire-and-forget, returns void). +-- The fault is reset and the suspended query is reaped. +-- +-- Extensions (gp_stats_collector, gp_inject_fault) come from setup.sql. + +CREATE TABLE qs_running_t (id int) DISTRIBUTED BY (id); +INSERT INTO qs_running_t SELECT generate_series(1, 100); + +-- Suspend execution on all primary segments. +SELECT gp_inject_fault('executor_pre_tuple_processed', 'suspend', dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + +-- Session 1: tag the connection, then launch a query that hangs on the QEs. +1: SET application_name TO 'qs_running_target'; +1&: SELECT count(*) FROM qs_running_t; + +-- Wait until the fault has been hit on the segments. +SELECT gp_wait_until_triggered_fault('executor_pre_tuple_processed', 1, dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + +-- Session 2: the running query has live QE backends, and polling succeeds. +2: SELECT count(*) > 0 AS has_backends FROM gpsc.pg_query_state_backends( + (SELECT pid FROM pg_stat_activity + WHERE application_name = 'qs_running_target' AND pid <> pg_backend_pid() + ORDER BY backend_start LIMIT 1)); +2: SELECT gpsc.pg_query_state( + (SELECT pid FROM pg_stat_activity + WHERE application_name = 'qs_running_target' AND pid <> pg_backend_pid() + ORDER BY backend_start LIMIT 1), + '\x00112233445566778899aabbccddeeff'::bytea); + +-- Release the fault and reap the suspended query. +SELECT gp_inject_fault('executor_pre_tuple_processed', 'reset', dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; +1<: +1q: +2q: + +DROP TABLE qs_running_t; diff --git a/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_seg_count.sql b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_seg_count.sql new file mode 100644 index 00000000000..d887f2e71c1 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/sql/gpsc_pqs_seg_count.sql @@ -0,0 +1,36 @@ +-- backends reports exactly one participating backend per primary segment for a +-- single-gang query -- a strict count rather than the has_backends>0 smoke +-- check in gpsc_pqs_running. +-- +-- A plain scan+count is one gang, so the QE list must match the number of +-- primary segments. +-- +-- Extensions come from setup.sql. + +CREATE TABLE qs_segcount_t (id int) DISTRIBUTED BY (id); +INSERT INTO qs_segcount_t SELECT generate_series(1, 100); + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'suspend', dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + +1: SET application_name TO 'qs_segcount_target'; +1&: SELECT count(*) FROM qs_segcount_t; + +SELECT gp_wait_until_triggered_fault('executor_pre_tuple_processed', 1, dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + +-- One backend per primary segment. +2: SELECT count(*) = (SELECT count(*) FROM gp_segment_configuration + WHERE role = 'p' AND content > -1) AS matches_primaries + FROM gpsc.pg_query_state_backends( + (SELECT pid FROM pg_stat_activity + WHERE application_name = 'qs_segcount_target' AND pid <> pg_backend_pid() + ORDER BY backend_start LIMIT 1)); + +SELECT gp_inject_fault('executor_pre_tuple_processed', 'reset', dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; +1<: +1q: +2q: + +DROP TABLE qs_segcount_t; diff --git a/gpcontrib/gp_stats_collector/test/isolation2/sql/setup.sql b/gpcontrib/gp_stats_collector/test/isolation2/sql/setup.sql new file mode 100644 index 00000000000..faec1135517 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/isolation2/sql/setup.sql @@ -0,0 +1,4 @@ +-- Shared setup for the pg_query_state isolation2 suite. +-- pg_isolation2_regress always runs a "setup" test before the schedule. +CREATE EXTENSION IF NOT EXISTS gp_stats_collector; +CREATE EXTENSION IF NOT EXISTS gp_inject_fault; diff --git a/gpcontrib/gp_stats_collector/test/sql/gpsc_pg_query_state.sql b/gpcontrib/gp_stats_collector/test/sql/gpsc_pg_query_state.sql new file mode 100644 index 00000000000..eb07c45afc2 --- /dev/null +++ b/gpcontrib/gp_stats_collector/test/sql/gpsc_pg_query_state.sql @@ -0,0 +1,46 @@ +-- pg_query_state signal API (extension v1.2): catalog contract + negative paths. +-- +-- Deterministic coverage only: SQL-visible function/type registration and the +-- input-validation error branches. The asynchronous happy path (poll a live +-- query and observe per-node stats) is exercised separately under isolation2, +-- since it depends on a second running backend and timing. +-- start_ignore +CREATE EXTENSION IF NOT EXISTS gp_stats_collector; +-- end_ignore + +-- +-- Catalog contract: the three SQL-visible functions are registered in the gpsc +-- schema with the expected return type and dispatch (exec) location. +-- proexeclocation: c = coordinator, a = any (QE-local), s = all segments. +-- +SELECT proname, + pronargs, + prorettype::regtype AS returns, + proexeclocation +FROM pg_proc +WHERE pronamespace = 'gpsc'::regnamespace + AND proname IN ('pg_query_state', 'pg_query_state_backends', 'cbdb_mpp_query_state') +ORDER BY proname; + +-- Composite identifier type used by the signal layer is present. +SELECT typname +FROM pg_type +WHERE typnamespace = 'gpsc'::regnamespace + AND typname = 'gp_segment_pid'; + +-- +-- Negative: a backend cannot poll its own state. +-- +SELECT gpsc.pg_query_state(pg_backend_pid(), '\x00112233445566778899aabbccddeeff'::bytea); +SELECT * FROM gpsc.pg_query_state_backends(pg_backend_pid()); + +-- +-- Negative: a pid that maps to no live backend is rejected. +-- +SELECT gpsc.pg_query_state(-1, '\x00112233445566778899aabbccddeeff'::bytea); +SELECT * FROM gpsc.pg_query_state_backends(-1); + +-- Cleanup +-- start_ignore +DROP EXTENSION gp_stats_collector; +-- end_ignore diff --git a/pom.xml b/pom.xml index 51a7830d5ae..a029c92d271 100644 --- a/pom.xml +++ b/pom.xml @@ -1280,6 +1280,9 @@ code or new licensing patterns. gpcontrib/gp_stats_collector/gp_stats_collector.control gpcontrib/gp_stats_collector/.clang-format gpcontrib/gp_stats_collector/Makefile + gpcontrib/gp_stats_collector/test/Makefile + gpcontrib/gp_stats_collector/test/isolation2/Makefile + gpcontrib/gp_stats_collector/test/isolation2/isolation2_schedule