This is an automated email from the ASF dual-hosted git repository.
ostinru pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudberry-pxf.git
The following commit(s) were added to refs/heads/main by this push:
new 3b5dead7 Observability: pxf_stat_activity and pxf_cancel_backend (#124)
3b5dead7 is described below
commit 3b5dead7146b8fea280c5e596c8bf52e9efeaaa9
Author: Nikolay Antonov <[email protected]>
AuthorDate: Sat Aug 29 16:17:58 2026 +0500
Observability: pxf_stat_activity and pxf_cancel_backend (#124)
### Add `pxf_stat_activity`
Add `pxf_stat_activity` to help DBAs find out what happening inside PXF
server.
```
postgres=# select * from pxf_stat_activity ;
-[ RECORD 1 ]-+---------------------------------------------------------
segment_id | 1
session_id | 57997
command_count | 8
xid | 57997-0000000008
operation | READ_BRIDGE
usename | gpadmin
server | ostinru-test
profile | s3:parquet
schema_name | public
table_name | test_blob_read
data_source | ostinru-test/topics/cuscom.email.created.v1/2026-05-29/*
query_start | 2026-06-23 16:16:48.318+03
pxf_host | example.com
```
### Add `pxf_cancel_backend` and `pxf_interrupt_backend`
Add `pxf_cancel_backend` and `pxf_interrupt_backend` to allow killing
running queries.
### Update tests
1. Update pxfExtension tests - use testconatiners.
2. Explicitly define extension version. So, there is no need in sql file
manipulations.
---
.../create_extension/expected/query01.ans | 91 ++++++++
.../create_extension/sql/query01.sql | 55 +++++
.../step_1_create_2_1/expected/query01.ans | 64 ++++++
.../step_1_create_2_1/sql/query01.sql | 46 ++++
.../step_2_after_upgrade_2_2/expected/query01.ans | 91 ++++++++
.../step_2_after_upgrade_2_2/sql/query01.sql | 55 +++++
.../expected/query01.ans | 64 ++++++
.../step_3_after_downgrade_2_1/sql/query01.sql | 46 ++++
.../features/extension/PxfExtensionTest.java | 18 ++
external-table/Makefile | 4 +-
external-table/pxf--2.1--2.2.sql | 99 ++++++++
external-table/pxf--2.2--2.1.sql | 39 ++++
external-table/pxf--2.2.sql | 130 +++++++++++
external-table/pxf.control | 2 +-
external-table/src/pxf_cancel_activity.c | 173 ++++++++++++++
external-table/src/pxf_stat_activity.c | 142 ++++++++++++
.../pxf/service/activity/ActiveRequest.java | 167 ++++++++++++++
.../pxf/service/activity/ActiveRequestInfo.java | 96 ++++++++
.../service/activity/ActiveRequestRegistry.java | 191 ++++++++++++++++
.../pxf/service/controller/BaseServiceImpl.java | 52 ++++-
.../pxf/service/controller/ReadServiceImpl.java | 30 ++-
.../pxf/service/controller/WriteServiceImpl.java | 27 ++-
.../service/rest/PxfBackendControlResource.java | 83 +++++++
.../pxf/service/rest/PxfStatActivityResource.java | 66 ++++++
.../activity/ActiveRequestRegistryTest.java | 250 +++++++++++++++++++++
.../service/controller/ReadServiceImplTest.java | 3 +-
.../service/controller/WriteServiceImplTest.java | 3 +-
.../service/rest/PxfBackendControlResourceIT.java | 92 ++++++++
.../service/rest/PxfStatActivityResourceIT.java | 89 ++++++++
29 files changed, 2242 insertions(+), 26 deletions(-)
diff --git
a/automation/sqlrepo/features/extension_tests_2_2/create_extension/expected/query01.ans
b/automation/sqlrepo/features/extension_tests_2_2/create_extension/expected/query01.ans
new file mode 100644
index 00000000..72792778
--- /dev/null
+++
b/automation/sqlrepo/features/extension_tests_2_2/create_extension/expected/query01.ans
@@ -0,0 +1,91 @@
+-- start_ignore
+-- end_ignore
+-- 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.
+
+-- @description query01 for PXF 2.2 install test (pxf_stat_activity + backend
control)
+-- start_matchsubs
+--
+-- m{.*/usr/local/pxf-(dev|gp\d).*}
+-- s{/usr/local/pxf-(dev|gp\d)}{\$PXF_HOME}
+--
+-- m{.*\$libdir/pxf.*}
+-- s{\$libdir}{\$PXF_HOME/gpextable}
+--
+-- end_matchsubs
+-- start_ignore
+\c pxfautomation_extension
+-- end_ignore
+
+SELECT extversion FROM pg_extension WHERE extname = 'pxf';
+ extversion
+------------
+ 2.2
+(1 row)
+
+SELECT p.proname
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+ proname
+---------------------------
+ pxf_cancel_backend
+ pxf_cancel_backend_raw
+ pxf_interrupt_backend
+ pxf_interrupt_backend_raw
+ pxf_read
+ pxf_stat_activity_raw
+ pxf_validate
+ pxf_write
+ pxfdelimited_import
+ pxfwritable_export
+ pxfwritable_import
+(11 rows)
+
+SELECT c.relname, c.relkind
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_class AS c ON (c.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+ relname | relkind
+-------------------+---------
+ pxf_stat_activity | v
+(1 row)
+
+-- the backend-control functions are reachable end-to-end (C -> local PXF ->
registry);
+-- with no in-flight requests they are no-ops returning 0
+SELECT pxf_cancel_backend(-1);
+ pxf_cancel_backend
+--------------------
+ 0
+(1 row)
+
+SELECT pxf_interrupt_backend(-1);
+ pxf_interrupt_backend
+-----------------------
+ 0
+(1 row)
+
+-- the activity view is empty while PXF is idle
+SELECT count(*) FROM pxf_stat_activity;
+ count
+-------
+ 0
+(1 row)
diff --git
a/automation/sqlrepo/features/extension_tests_2_2/create_extension/sql/query01.sql
b/automation/sqlrepo/features/extension_tests_2_2/create_extension/sql/query01.sql
new file mode 100644
index 00000000..dc067126
--- /dev/null
+++
b/automation/sqlrepo/features/extension_tests_2_2/create_extension/sql/query01.sql
@@ -0,0 +1,55 @@
+-- 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.
+
+-- @description query01 for PXF 2.2 install test (pxf_stat_activity + backend
control)
+-- start_matchsubs
+--
+-- m{.*/usr/local/pxf-(dev|gp\d).*}
+-- s{/usr/local/pxf-(dev|gp\d)}{\$PXF_HOME}
+--
+-- m{.*\$libdir/pxf.*}
+-- s{\$libdir}{\$PXF_HOME/gpextable}
+--
+-- end_matchsubs
+-- start_ignore
+\c pxfautomation_extension
+-- end_ignore
+
+SELECT extversion FROM pg_extension WHERE extname = 'pxf';
+
+SELECT p.proname
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+
+SELECT c.relname, c.relkind
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_class AS c ON (c.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+
+-- the backend-control functions are reachable end-to-end (C -> local PXF ->
registry);
+-- with no in-flight requests they are no-ops returning 0
+SELECT pxf_cancel_backend(-1);
+
+SELECT pxf_interrupt_backend(-1);
+
+-- the activity view is empty while PXF is idle
+SELECT count(*) FROM pxf_stat_activity;
diff --git
a/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_1_create_2_1/expected/query01.ans
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_1_create_2_1/expected/query01.ans
new file mode 100644
index 00000000..b1f54f67
--- /dev/null
+++
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_1_create_2_1/expected/query01.ans
@@ -0,0 +1,64 @@
+-- start_ignore
+-- end_ignore
+-- 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.
+
+-- @description query01 for PXF 2.2 upgrade/downgrade test - step 1: create at
2.1
+-- start_matchsubs
+--
+-- m{.*/usr/local/pxf-(dev|gp\d).*}
+-- s{/usr/local/pxf-(dev|gp\d)}{\$PXF_HOME}
+--
+-- m{.*\$libdir/pxf.*}
+-- s{\$libdir}{\$PXF_HOME/gpextable}
+--
+-- end_matchsubs
+-- start_ignore
+\c pxfautomation_extension
+-- end_ignore
+
+SELECT extversion FROM pg_extension WHERE extname = 'pxf';
+ extversion
+------------
+ 2.1
+(1 row)
+
+SELECT p.proname
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+ proname
+---------------------
+ pxf_read
+ pxf_validate
+ pxf_write
+ pxfdelimited_import
+ pxfwritable_export
+ pxfwritable_import
+(6 rows)
+
+SELECT c.relname, c.relkind
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_class AS c ON (c.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+ relname | relkind
+---------+---------
+(0 rows)
diff --git
a/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_1_create_2_1/sql/query01.sql
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_1_create_2_1/sql/query01.sql
new file mode 100644
index 00000000..c708069b
--- /dev/null
+++
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_1_create_2_1/sql/query01.sql
@@ -0,0 +1,46 @@
+-- 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.
+
+-- @description query01 for PXF 2.2 upgrade/downgrade test - step 1: create at
2.1
+-- start_matchsubs
+--
+-- m{.*/usr/local/pxf-(dev|gp\d).*}
+-- s{/usr/local/pxf-(dev|gp\d)}{\$PXF_HOME}
+--
+-- m{.*\$libdir/pxf.*}
+-- s{\$libdir}{\$PXF_HOME/gpextable}
+--
+-- end_matchsubs
+-- start_ignore
+\c pxfautomation_extension
+-- end_ignore
+
+SELECT extversion FROM pg_extension WHERE extname = 'pxf';
+
+SELECT p.proname
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+
+SELECT c.relname, c.relkind
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_class AS c ON (c.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
diff --git
a/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_2_after_upgrade_2_2/expected/query01.ans
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_2_after_upgrade_2_2/expected/query01.ans
new file mode 100644
index 00000000..4d1c0a1c
--- /dev/null
+++
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_2_after_upgrade_2_2/expected/query01.ans
@@ -0,0 +1,91 @@
+-- start_ignore
+-- end_ignore
+-- 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.
+
+-- @description query01 for PXF 2.2 upgrade/downgrade test - step 2: after
upgrade to 2.2
+-- start_matchsubs
+--
+-- m{.*/usr/local/pxf-(dev|gp\d).*}
+-- s{/usr/local/pxf-(dev|gp\d)}{\$PXF_HOME}
+--
+-- m{.*\$libdir/pxf.*}
+-- s{\$libdir}{\$PXF_HOME/gpextable}
+--
+-- end_matchsubs
+-- start_ignore
+\c pxfautomation_extension
+-- end_ignore
+
+SELECT extversion FROM pg_extension WHERE extname = 'pxf';
+ extversion
+------------
+ 2.2
+(1 row)
+
+SELECT p.proname
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+ proname
+---------------------------
+ pxf_cancel_backend
+ pxf_cancel_backend_raw
+ pxf_interrupt_backend
+ pxf_interrupt_backend_raw
+ pxf_read
+ pxf_stat_activity_raw
+ pxf_validate
+ pxf_write
+ pxfdelimited_import
+ pxfwritable_export
+ pxfwritable_import
+(11 rows)
+
+SELECT c.relname, c.relkind
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_class AS c ON (c.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+ relname | relkind
+-------------------+---------
+ pxf_stat_activity | v
+(1 row)
+
+-- the backend-control functions are reachable end-to-end (C -> local PXF ->
registry);
+-- with no in-flight requests they are no-ops returning 0
+SELECT pxf_cancel_backend(-1);
+ pxf_cancel_backend
+--------------------
+ 0
+(1 row)
+
+SELECT pxf_interrupt_backend(-1);
+ pxf_interrupt_backend
+-----------------------
+ 0
+(1 row)
+
+-- the activity view is empty while PXF is idle
+SELECT count(*) FROM pxf_stat_activity;
+ count
+-------
+ 0
+(1 row)
diff --git
a/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_2_after_upgrade_2_2/sql/query01.sql
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_2_after_upgrade_2_2/sql/query01.sql
new file mode 100644
index 00000000..f83ae490
--- /dev/null
+++
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_2_after_upgrade_2_2/sql/query01.sql
@@ -0,0 +1,55 @@
+-- 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.
+
+-- @description query01 for PXF 2.2 upgrade/downgrade test - step 2: after
upgrade to 2.2
+-- start_matchsubs
+--
+-- m{.*/usr/local/pxf-(dev|gp\d).*}
+-- s{/usr/local/pxf-(dev|gp\d)}{\$PXF_HOME}
+--
+-- m{.*\$libdir/pxf.*}
+-- s{\$libdir}{\$PXF_HOME/gpextable}
+--
+-- end_matchsubs
+-- start_ignore
+\c pxfautomation_extension
+-- end_ignore
+
+SELECT extversion FROM pg_extension WHERE extname = 'pxf';
+
+SELECT p.proname
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+
+SELECT c.relname, c.relkind
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_class AS c ON (c.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+
+-- the backend-control functions are reachable end-to-end (C -> local PXF ->
registry);
+-- with no in-flight requests they are no-ops returning 0
+SELECT pxf_cancel_backend(-1);
+
+SELECT pxf_interrupt_backend(-1);
+
+-- the activity view is empty while PXF is idle
+SELECT count(*) FROM pxf_stat_activity;
diff --git
a/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_3_after_downgrade_2_1/expected/query01.ans
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_3_after_downgrade_2_1/expected/query01.ans
new file mode 100644
index 00000000..f91bb17b
--- /dev/null
+++
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_3_after_downgrade_2_1/expected/query01.ans
@@ -0,0 +1,64 @@
+-- start_ignore
+-- end_ignore
+-- 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.
+
+-- @description query01 for PXF 2.2 upgrade/downgrade test - step 3: after
downgrade to 2.1
+-- start_matchsubs
+--
+-- m{.*/usr/local/pxf-(dev|gp\d).*}
+-- s{/usr/local/pxf-(dev|gp\d)}{\$PXF_HOME}
+--
+-- m{.*\$libdir/pxf.*}
+-- s{\$libdir}{\$PXF_HOME/gpextable}
+--
+-- end_matchsubs
+-- start_ignore
+\c pxfautomation_extension
+-- end_ignore
+
+SELECT extversion FROM pg_extension WHERE extname = 'pxf';
+ extversion
+------------
+ 2.1
+(1 row)
+
+SELECT p.proname
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+ proname
+---------------------
+ pxf_read
+ pxf_validate
+ pxf_write
+ pxfdelimited_import
+ pxfwritable_export
+ pxfwritable_import
+(6 rows)
+
+SELECT c.relname, c.relkind
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_class AS c ON (c.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+ relname | relkind
+---------+---------
+(0 rows)
diff --git
a/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_3_after_downgrade_2_1/sql/query01.sql
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_3_after_downgrade_2_1/sql/query01.sql
new file mode 100644
index 00000000..6f28f334
--- /dev/null
+++
b/automation/sqlrepo/features/extension_tests_2_2/upgrade_downgrade/step_3_after_downgrade_2_1/sql/query01.sql
@@ -0,0 +1,46 @@
+-- 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.
+
+-- @description query01 for PXF 2.2 upgrade/downgrade test - step 3: after
downgrade to 2.1
+-- start_matchsubs
+--
+-- m{.*/usr/local/pxf-(dev|gp\d).*}
+-- s{/usr/local/pxf-(dev|gp\d)}{\$PXF_HOME}
+--
+-- m{.*\$libdir/pxf.*}
+-- s{\$libdir}{\$PXF_HOME/gpextable}
+--
+-- end_matchsubs
+-- start_ignore
+\c pxfautomation_extension
+-- end_ignore
+
+SELECT extversion FROM pg_extension WHERE extname = 'pxf';
+
+SELECT p.proname
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
+
+SELECT c.relname, c.relkind
+FROM pg_catalog.pg_extension AS e
+ INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
+ INNER JOIN pg_catalog.pg_class AS c ON (c.oid = d.objid)
+WHERE d.deptype = 'e' AND e.extname = 'pxf'
+ORDER BY 1;
diff --git
a/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/extension/PxfExtensionTest.java
b/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/extension/PxfExtensionTest.java
index 77cbaaa3..4ebf6c4a 100644
---
a/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/extension/PxfExtensionTest.java
+++
b/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/extension/PxfExtensionTest.java
@@ -100,4 +100,22 @@ public class PxfExtensionTest extends
AbstractTestcontainersTest {
extensionDb.runQuery("ALTER EXTENSION pxf UPDATE TO '2.1'");
regress.runSqlTest("features/extension_tests/downgrade_then_upgrade/step_3_after_alter_extension_upgrade");
}
+
+ @Test(groups = {"testcontainers", "pxf-extension"})
+ public void testPxfCreateExtension22() throws Exception {
+ extensionDb.runQuery("CREATE EXTENSION pxf VERSION '2.2'");
+ regress.runSqlTest("features/extension_tests_2_2/create_extension");
+ }
+
+ @Test(groups = {"testcontainers", "pxf-extension"})
+ public void testPxfUpgradeDowngrade22() throws Exception {
+ extensionDb.runQuery("CREATE EXTENSION pxf VERSION '2.1'");
+
regress.runSqlTest("features/extension_tests_2_2/upgrade_downgrade/step_1_create_2_1");
+
+ extensionDb.runQuery("ALTER EXTENSION pxf UPDATE TO '2.2'");
+
regress.runSqlTest("features/extension_tests_2_2/upgrade_downgrade/step_2_after_upgrade_2_2");
+
+ extensionDb.runQuery("ALTER EXTENSION pxf UPDATE TO '2.1'");
+
regress.runSqlTest("features/extension_tests_2_2/upgrade_downgrade/step_3_after_downgrade_2_1");
+ }
}
diff --git a/external-table/Makefile b/external-table/Makefile
index ae5195dc..ebead67b 100644
--- a/external-table/Makefile
+++ b/external-table/Makefile
@@ -1,7 +1,7 @@
EXTENSION = pxf
-DATA = pxf--2.1.sql pxf--2.0--2.1.sql pxf--2.1--2.0.sql pxf--2.0.sql
pxf--1.0--2.0.sql pxf--1.0.sql
+DATA = pxf--2.2.sql pxf--2.1--2.2.sql pxf--2.2--2.1.sql pxf--2.1.sql
pxf--2.0--2.1.sql pxf--2.1--2.0.sql pxf--2.0.sql pxf--1.0--2.0.sql pxf--1.0.sql
MODULE_big = pxf
-OBJS = src/pxfprotocol.o src/pxfbridge.o src/pxfuriparser.o
src/libchurl.o src/pxfutils.o src/pxfheaders.o src/gpdbwritableformatter.o
src/pxfdelimited_formatter.o src/pxffilters.o
+OBJS = src/pxfprotocol.o src/pxfbridge.o src/pxfuriparser.o
src/libchurl.o src/pxfutils.o src/pxfheaders.o src/gpdbwritableformatter.o
src/pxfdelimited_formatter.o src/pxffilters.o src/pxf_stat_activity.o
src/pxf_cancel_activity.o
REGRESS = setup pxf pxfinvalid
SHLIB_LINK += -lcurl
diff --git a/external-table/pxf--2.1--2.2.sql b/external-table/pxf--2.1--2.2.sql
new file mode 100644
index 00000000..bdfd87a7
--- /dev/null
+++ b/external-table/pxf--2.1--2.2.sql
@@ -0,0 +1,99 @@
+-- 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.
+
+/* external_table/pxf--2.1--2.2.sql */
+
+------------------------------------------------------------------
+-- PXF Activity Monitoring
+------------------------------------------------------------------
+
+-- Raw per-segment accessor: each segment asks its local PXF instance for the
+-- activity that originates from its own segment id and returns the JSON body
+-- verbatim as a single row. Dispatched to every segment; the typed columns are
+-- produced by the pxf_stat_activity view below. The function is set-returning
+-- (one row per segment) because EXECUTE ON ALL SEGMENTS is only permitted for
+-- set-returning functions.
+CREATE FUNCTION pxf_stat_activity_raw() RETURNS SETOF text
+AS 'MODULE_PATHNAME', 'pxf_stat_activity_raw'
+LANGUAGE C VOLATILE EXECUTE ON ALL SEGMENTS;
+
+-- pg_stat_activity-like view of the queries currently running inside PXF,
+-- aggregated across all segment hosts. DISTINCT is a safety net; PXF already
+-- de-duplicates by filtering each response to the requesting segment id.
+CREATE VIEW pxf_stat_activity AS
+SELECT DISTINCT
+ (a->>'segmentId')::int AS segment_id,
+ (a->>'gpSessionId')::int AS session_id,
+ (a->>'gpCommandCount')::int AS command_count,
+ a->>'transactionId' AS xid,
+ a->>'requestType' AS operation,
+ a->>'user' AS usename,
+ a->>'serverName' AS server,
+ a->>'profile' AS profile,
+ a->>'schemaName' AS schema_name,
+ a->>'tableName' AS table_name,
+ a->>'dataSource' AS data_source,
+ to_timestamp((a->>'startTimeMs')::bigint / 1000.0) AS query_start,
+ a->>'host' AS pxf_host
+FROM (
+ SELECT json_array_elements(raw::json -> 'activities') AS a
+ FROM pxf_stat_activity_raw() AS raw
+) s;
+
+-- Per-segment cancellation primitives backing pxf_cancel_backend /
+-- pxf_interrupt_backend. Each segment asks its local PXF instance to terminate
+-- the in-flight requests of the given Cloudberry session that originate from
its
+-- own segment id, returning the JSON body verbatim as a single row (e.g.
+-- {"cancelled":N} / {"interrupted":N}). Dispatched to every segment; the
counts
+-- are summed by the SQL wrappers below. Set-returning because EXECUTE ON ALL
+-- SEGMENTS is only permitted for set-returning functions.
+CREATE FUNCTION pxf_cancel_backend_raw(session_id int) RETURNS SETOF text
+AS 'MODULE_PATHNAME', 'pxf_cancel_backend_raw'
+LANGUAGE C VOLATILE STRICT EXECUTE ON ALL SEGMENTS;
+
+CREATE FUNCTION pxf_interrupt_backend_raw(session_id int) RETURNS SETOF text
+AS 'MODULE_PATHNAME', 'pxf_interrupt_backend_raw'
+LANGUAGE C VOLATILE STRICT EXECUTE ON ALL SEGMENTS;
+
+-- Gracefully cancels the in-flight PXF requests of a Cloudberry session across
+-- the whole cluster by ending their current bridge. Returns the number of
+-- requests that were signalled. Analogous to pg_cancel_backend, but keyed by
+-- the Cloudberry session id (as reported in pxf_stat_activity.session_id).
+CREATE FUNCTION pxf_cancel_backend(session_id int) RETURNS int AS $$
+ SELECT coalesce(sum((raw::json ->> 'cancelled')::int), 0)::int
+ FROM pxf_cancel_backend_raw(session_id) AS raw
+$$ LANGUAGE sql VOLATILE;
+
+-- Interrupts the worker thread(s) of the in-flight PXF requests of a
Cloudberry
+-- session across the whole cluster. Returns the number of requests that were
+-- interrupted. A forceful complement to pxf_cancel_backend for requests that
do
+-- not observe cancellation (e.g. blocked in a non-interruptible read).
+CREATE FUNCTION pxf_interrupt_backend(session_id int) RETURNS int AS $$
+ SELECT coalesce(sum((raw::json ->> 'interrupted')::int), 0)::int
+ FROM pxf_interrupt_backend_raw(session_id) AS raw
+$$ LANGUAGE sql VOLATILE;
+
+-- Functions default to EXECUTE for PUBLIC, which would let any user observe
and
+-- cancel other users' PXF requests (session ids are trivially enumerable).
+-- Superusers bypass ACLs; delegate explicitly with e.g.
+-- GRANT EXECUTE ON FUNCTION pxf_cancel_backend(int) TO monitoring_role;
+-- The wrappers are plain SQL, so the _raw primitives must be revoked as well.
+REVOKE ALL ON FUNCTION pxf_stat_activity_raw() FROM PUBLIC;
+REVOKE ALL ON FUNCTION pxf_cancel_backend_raw(int) FROM PUBLIC;
+REVOKE ALL ON FUNCTION pxf_interrupt_backend_raw(int) FROM PUBLIC;
+REVOKE ALL ON FUNCTION pxf_cancel_backend(int) FROM PUBLIC;
+REVOKE ALL ON FUNCTION pxf_interrupt_backend(int) FROM PUBLIC;
diff --git a/external-table/pxf--2.2--2.1.sql b/external-table/pxf--2.2--2.1.sql
new file mode 100644
index 00000000..5bcacf59
--- /dev/null
+++ b/external-table/pxf--2.2--2.1.sql
@@ -0,0 +1,39 @@
+-- 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.
+
+/* external_table/pxf--2.2--2.1.sql */
+
+------------------------------------------------------------------
+-- PXF Activity Monitoring
+------------------------------------------------------------------
+
+-- remove the objects from the extension
+ALTER EXTENSION pxf DROP FUNCTION pxf_interrupt_backend(int);
+ALTER EXTENSION pxf DROP FUNCTION pxf_cancel_backend(int);
+ALTER EXTENSION pxf DROP FUNCTION pxf_interrupt_backend_raw(int);
+ALTER EXTENSION pxf DROP FUNCTION pxf_cancel_backend_raw(int);
+ALTER EXTENSION pxf DROP VIEW pxf_stat_activity;
+ALTER EXTENSION pxf DROP FUNCTION pxf_stat_activity_raw();
+
+-- remove the objects themselves from the catalog; the view depends on
+-- pxf_stat_activity_raw(), so it has to go first
+DROP FUNCTION pxf_interrupt_backend(int);
+DROP FUNCTION pxf_cancel_backend(int);
+DROP FUNCTION pxf_interrupt_backend_raw(int);
+DROP FUNCTION pxf_cancel_backend_raw(int);
+DROP VIEW pxf_stat_activity;
+DROP FUNCTION pxf_stat_activity_raw();
diff --git a/external-table/pxf--2.2.sql b/external-table/pxf--2.2.sql
new file mode 100644
index 00000000..927f42bf
--- /dev/null
+++ b/external-table/pxf--2.2.sql
@@ -0,0 +1,130 @@
+-- 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.
+
+------------------------------------------------------------------
+-- PXF Protocol/Formatters
+------------------------------------------------------------------
+
+CREATE OR REPLACE FUNCTION pg_catalog.pxf_write() RETURNS integer
+AS 'MODULE_PATHNAME', 'pxfprotocol_export'
+LANGUAGE C STABLE;
+
+CREATE OR REPLACE FUNCTION pg_catalog.pxf_read() RETURNS integer
+AS 'MODULE_PATHNAME', 'pxfprotocol_import'
+LANGUAGE C STABLE;
+
+CREATE OR REPLACE FUNCTION pg_catalog.pxf_validate() RETURNS void
+AS 'MODULE_PATHNAME', 'pxfprotocol_validate_urls'
+LANGUAGE C STABLE;
+
+CREATE OR REPLACE FUNCTION pg_catalog.pxfwritable_import() RETURNS record
+AS 'MODULE_PATHNAME', 'gpdbwritableformatter_import'
+LANGUAGE C STABLE;
+
+CREATE OR REPLACE FUNCTION pg_catalog.pxfwritable_export(record) RETURNS bytea
+AS 'MODULE_PATHNAME', 'gpdbwritableformatter_export'
+LANGUAGE C STABLE;
+
+CREATE OR REPLACE FUNCTION pg_catalog.pxfdelimited_import() RETURNS record
+AS 'MODULE_PATHNAME', 'pxfdelimited_import'
+LANGUAGE C STABLE;
+
+CREATE TRUSTED PROTOCOL pxf (
+ writefunc = pxf_write,
+ readfunc = pxf_read,
+ validatorfunc = pxf_validate);
+
+------------------------------------------------------------------
+-- PXF Activity Monitoring
+------------------------------------------------------------------
+
+-- Raw per-segment accessor: each segment asks its local PXF instance for the
+-- activity that originates from its own segment id and returns the JSON body
+-- verbatim as a single row. Dispatched to every segment; the typed columns are
+-- produced by the pxf_stat_activity view below. The function is set-returning
+-- (one row per segment) because EXECUTE ON ALL SEGMENTS is only permitted for
+-- set-returning functions.
+CREATE FUNCTION pxf_stat_activity_raw() RETURNS SETOF text
+AS 'MODULE_PATHNAME', 'pxf_stat_activity_raw'
+LANGUAGE C VOLATILE EXECUTE ON ALL SEGMENTS;
+
+-- pg_stat_activity-like view of the queries currently running inside PXF,
+-- aggregated across all segment hosts. DISTINCT is a safety net; PXF already
+-- de-duplicates by filtering each response to the requesting segment id.
+CREATE VIEW pxf_stat_activity AS
+SELECT DISTINCT
+ (a->>'segmentId')::int AS segment_id,
+ (a->>'gpSessionId')::int AS session_id,
+ (a->>'gpCommandCount')::int AS command_count,
+ a->>'transactionId' AS xid,
+ a->>'requestType' AS operation,
+ a->>'user' AS usename,
+ a->>'serverName' AS server,
+ a->>'profile' AS profile,
+ a->>'schemaName' AS schema_name,
+ a->>'tableName' AS table_name,
+ a->>'dataSource' AS data_source,
+ to_timestamp((a->>'startTimeMs')::bigint / 1000.0) AS query_start,
+ a->>'host' AS pxf_host
+FROM (
+ SELECT json_array_elements(raw::json -> 'activities') AS a
+ FROM pxf_stat_activity_raw() AS raw
+) s;
+
+-- Per-segment cancellation primitives backing pxf_cancel_backend /
+-- pxf_interrupt_backend. Each segment asks its local PXF instance to terminate
+-- the in-flight requests of the given Cloudberry session that originate from
its
+-- own segment id, returning the JSON body verbatim as a single row (e.g.
+-- {"cancelled":N} / {"interrupted":N}). Dispatched to every segment; the
counts
+-- are summed by the SQL wrappers below. Set-returning because EXECUTE ON ALL
+-- SEGMENTS is only permitted for set-returning functions.
+CREATE FUNCTION pxf_cancel_backend_raw(session_id int) RETURNS SETOF text
+AS 'MODULE_PATHNAME', 'pxf_cancel_backend_raw'
+LANGUAGE C VOLATILE STRICT EXECUTE ON ALL SEGMENTS;
+
+CREATE FUNCTION pxf_interrupt_backend_raw(session_id int) RETURNS SETOF text
+AS 'MODULE_PATHNAME', 'pxf_interrupt_backend_raw'
+LANGUAGE C VOLATILE STRICT EXECUTE ON ALL SEGMENTS;
+
+-- Gracefully cancels the in-flight PXF requests of a Cloudberry session across
+-- the whole cluster by ending their current bridge. Returns the number of
+-- requests that were signalled. Analogous to pg_cancel_backend, but keyed by
+-- the Cloudberry session id (as reported in pxf_stat_activity.session_id).
+CREATE FUNCTION pxf_cancel_backend(session_id int) RETURNS int AS $$
+ SELECT coalesce(sum((raw::json ->> 'cancelled')::int), 0)::int
+ FROM pxf_cancel_backend_raw(session_id) AS raw
+$$ LANGUAGE sql VOLATILE;
+
+-- Interrupts the worker thread(s) of the in-flight PXF requests of a
Cloudberry
+-- session across the whole cluster. Returns the number of requests that were
+-- interrupted. A forceful complement to pxf_cancel_backend for requests that
do
+-- not observe cancellation (e.g. blocked in a non-interruptible read).
+CREATE FUNCTION pxf_interrupt_backend(session_id int) RETURNS int AS $$
+ SELECT coalesce(sum((raw::json ->> 'interrupted')::int), 0)::int
+ FROM pxf_interrupt_backend_raw(session_id) AS raw
+$$ LANGUAGE sql VOLATILE;
+
+-- Functions default to EXECUTE for PUBLIC, which would let any user observe
and
+-- cancel other users' PXF requests (session ids are trivially enumerable).
+-- Superusers bypass ACLs; delegate explicitly with e.g.
+-- GRANT EXECUTE ON FUNCTION pxf_cancel_backend(int) TO monitoring_role;
+-- The wrappers are plain SQL, so the _raw primitives must be revoked as well.
+REVOKE ALL ON FUNCTION pxf_stat_activity_raw() FROM PUBLIC;
+REVOKE ALL ON FUNCTION pxf_cancel_backend_raw(int) FROM PUBLIC;
+REVOKE ALL ON FUNCTION pxf_interrupt_backend_raw(int) FROM PUBLIC;
+REVOKE ALL ON FUNCTION pxf_cancel_backend(int) FROM PUBLIC;
+REVOKE ALL ON FUNCTION pxf_interrupt_backend(int) FROM PUBLIC;
diff --git a/external-table/pxf.control b/external-table/pxf.control
index ca0ae18a..ef773738 100644
--- a/external-table/pxf.control
+++ b/external-table/pxf.control
@@ -1,5 +1,5 @@
directory = 'extension'
-default_version = '2.1'
+default_version = '2.2'
comment = 'Extension which allows to access unmanaged data'
module_pathname = '$libdir/pxf'
superuser = true
diff --git a/external-table/src/pxf_cancel_activity.c
b/external-table/src/pxf_cancel_activity.c
new file mode 100644
index 00000000..4bb16abe
--- /dev/null
+++ b/external-table/src/pxf_cancel_activity.c
@@ -0,0 +1,173 @@
+/*
+ * 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.
+ */
+
+/*
+ * pxf_cancel_activity.c
+ *
+ * Backs the pxf_cancel_backend and pxf_interrupt_backend SQL functions. Each
is
+ * dispatched with EXECUTE ON ALL SEGMENTS: every segment asks its local PXF
+ * instance to terminate the in-flight requests of a given Greenplum session
+ * that originate from its own segment id (passed via X-GP-SEGMENT-ID).
Because a
+ * single PXF instance serves every segment co-located on the host, this
+ * per-segment filter guarantees each running request is acted on exactly once,
+ * by its owning segment.
+ *
+ * Both functions are set-returning (one row per segment, carrying the PXF
+ * response verbatim as a JSON text value) because EXECUTE ON ALL SEGMENTS is
+ * only permitted for set-returning functions; the counts are summed by the SQL
+ * wrappers, keeping this layer schema-agnostic.
+ */
+
+#include "libchurl.h"
+#include "pxfuriparser.h"
+#include "pxfutils.h"
+
+#include "funcapi.h"
+#include "cdb/cdbvars.h"
+#include "utils/builtins.h"
+
+#define PXF_CANCEL_READ_BUFFER_SIZE (64 * 1024)
+
+PG_FUNCTION_INFO_V1(pxf_cancel_backend_raw);
+PG_FUNCTION_INFO_V1(pxf_interrupt_backend_raw);
+
+Datum pxf_cancel_backend_raw(PG_FUNCTION_ARGS);
+Datum pxf_interrupt_backend_raw(PG_FUNCTION_ARGS);
+
+/*
+ * Issues an HTTP GET to the given local PXF endpoint, scoped to this segment
and
+ * the target Greenplum session, and returns the raw JSON response body as a
+ * palloc'd text value in the current memory context.
+ */
+static text *
+fetch_backend_control_body(const char *endpoint, int32 session_id)
+{
+ CHURL_HEADERS headers;
+ CHURL_HANDLE handle;
+ StringInfoData uri;
+ StringInfoData response;
+ char readbuf[PXF_CANCEL_READ_BUFFER_SIZE];
+ char segment_id[32];
+ char session_buf[32];
+ size_t n;
+ text *result;
+
+ /* build the request URI for the local PXF instance */
+ initStringInfo(&uri);
+ appendStringInfo(&uri, "http://%s/%s/%s",
+ get_authority(), PXF_SERVICE_PREFIX,
endpoint);
+
+ /* scope the request to this segment so co-located segments don't act
twice */
+ snprintf(segment_id, sizeof(segment_id), "%d", GpIdentity.segindex);
+ snprintf(session_buf, sizeof(session_buf), "%d", session_id);
+
+ headers = churl_headers_init();
+ churl_headers_append(headers, "X-GP-SEGMENT-ID", segment_id);
+ churl_headers_append(headers, "X-GP-SESSION-ID", session_buf);
+ churl_headers_append(headers, "Accept", "application/json");
+
+ elog(DEBUG2, "%s: segment %d requesting %s for session %d",
+ endpoint, GpIdentity.segindex, uri.data, session_id);
+
+ handle = churl_init_download(uri.data, headers);
+
+ /* read some bytes to make sure the connection is established */
+ churl_read_check_connectivity(handle);
+
+ /* read the full JSON body */
+ initStringInfo(&response);
+ while ((n = churl_read(handle, readbuf, sizeof(readbuf))) != 0)
+ appendBinaryStringInfo(&response, readbuf, n);
+
+ /* surface any error reported by the PXF service on the closed
connection */
+ churl_read_check_connectivity(handle);
+
+ churl_cleanup(handle, false);
+ churl_headers_cleanup(headers);
+
+ result = cstring_to_text(response.data);
+
+ pfree(uri.data);
+ pfree(response.data);
+
+ return result;
+}
+
+/*
+ * Shared set-returning body: emit a single row carrying the raw JSON response
+ * from the local PXF instance for the given endpoint and session id.
Dispatched
+ * with EXECUTE ON ALL SEGMENTS (which Cloudberry only permits for
set-returning
+ * functions); the pxf_cancel_backend / pxf_interrupt_backend SQL wrappers sum
+ * the per-segment counts.
+ */
+static Datum
+backend_control_srf(FunctionCallInfo fcinfo, const char *endpoint)
+{
+ FuncCallContext *funcctx;
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ int32 session_id = PG_GETARG_INT32(0);
+
+ funcctx = SRF_FIRSTCALL_INIT();
+
+ /* the fetched body must outlive this call, so build it in the
+ * multi-call context and hand it back one row at a time */
+ oldcontext =
MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ funcctx->user_fctx = fetch_backend_control_body(endpoint,
session_id);
+ funcctx->max_calls = 1;
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+
+ if (funcctx->call_cntr < funcctx->max_calls)
+ SRF_RETURN_NEXT(funcctx, PointerGetDatum((text *)
funcctx->user_fctx));
+
+ SRF_RETURN_DONE(funcctx);
+}
+
+/*
+ * pxf_cancel_backend_raw(session_id int)
+ *
+ * Asks the local PXF instance to gracefully cancel the in-flight requests of
the
+ * given session (by ending their current bridge). Returns the raw JSON body,
+ * e.g. {"cancelled":N}.
+ */
+Datum
+pxf_cancel_backend_raw(PG_FUNCTION_ARGS)
+{
+ return backend_control_srf(fcinfo, "cancel_backend");
+}
+
+/*
+ * pxf_interrupt_backend_raw(session_id int)
+ *
+ * Asks the local PXF instance to interrupt the worker thread(s) of the
in-flight
+ * requests of the given session. Returns the raw JSON body, e.g.
+ * {"interrupted":N}.
+ */
+Datum
+pxf_interrupt_backend_raw(PG_FUNCTION_ARGS)
+{
+ return backend_control_srf(fcinfo, "interrupt_backend");
+}
diff --git a/external-table/src/pxf_stat_activity.c
b/external-table/src/pxf_stat_activity.c
new file mode 100644
index 00000000..21ca7620
--- /dev/null
+++ b/external-table/src/pxf_stat_activity.c
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+
+/*
+ * pxf_stat_activity.c
+ *
+ * Backs the pxf_stat_activity SQL view. The function pxf_stat_activity_raw()
+ * is dispatched with EXECUTE ON ALL SEGMENTS: each segment asks its local PXF
+ * instance for the requests that originate from its own segment id (passed via
+ * the X-GP-SEGMENT-ID header). Because a single PXF instance serves every
+ * segment co-located on the host, this per-segment filter guarantees each
+ * running query is reported exactly once, by its owning segment.
+ *
+ * The function is set-returning (one row per segment, carrying the PXF
response
+ * verbatim as a JSON text value) because EXECUTE ON ALL SEGMENTS is only
+ * permitted for set-returning functions; the typed columns are produced by the
+ * SQL view, keeping this layer schema-agnostic.
+ */
+
+#include "libchurl.h"
+#include "pxfuriparser.h"
+#include "pxfutils.h"
+
+#include "funcapi.h"
+#include "cdb/cdbvars.h"
+#include "utils/builtins.h"
+
+#define PXF_STAT_READ_BUFFER_SIZE (64 * 1024)
+
+PG_FUNCTION_INFO_V1(pxf_stat_activity_raw);
+
+Datum pxf_stat_activity_raw(PG_FUNCTION_ARGS);
+
+/*
+ * Issues an HTTP GET to the local PXF /pxf/stat_activity endpoint, scoped to
+ * this segment, and returns the raw JSON response body as a palloc'd text
value
+ * in the current memory context.
+ */
+static text *
+fetch_stat_activity_body(void)
+{
+ CHURL_HEADERS headers;
+ CHURL_HANDLE handle;
+ StringInfoData uri;
+ StringInfoData response;
+ char readbuf[PXF_STAT_READ_BUFFER_SIZE];
+ char segment_id[32];
+ size_t n;
+ text *result;
+
+ /* build the request URI for the local PXF instance */
+ initStringInfo(&uri);
+ appendStringInfo(&uri, "http://%s/%s/stat_activity",
+ get_authority(), PXF_SERVICE_PREFIX);
+
+ /* scope the request to this segment so co-located segments don't
duplicate */
+ snprintf(segment_id, sizeof(segment_id), "%d", GpIdentity.segindex);
+
+ headers = churl_headers_init();
+ churl_headers_append(headers, "X-GP-SEGMENT-ID", segment_id);
+ churl_headers_append(headers, "Accept", "application/json");
+
+ elog(DEBUG2, "pxf_stat_activity: segment %d requesting %s",
+ GpIdentity.segindex, uri.data);
+
+ handle = churl_init_download(uri.data, headers);
+
+ /* read some bytes to make sure the connection is established */
+ churl_read_check_connectivity(handle);
+
+ /* read the full JSON body */
+ initStringInfo(&response);
+ while ((n = churl_read(handle, readbuf, sizeof(readbuf))) != 0)
+ appendBinaryStringInfo(&response, readbuf, n);
+
+ /* surface any error reported by the PXF service on the closed
connection */
+ churl_read_check_connectivity(handle);
+
+ churl_cleanup(handle, false);
+ churl_headers_cleanup(headers);
+
+ result = cstring_to_text(response.data);
+
+ pfree(uri.data);
+ pfree(response.data);
+
+ return result;
+}
+
+/*
+ * pxf_stat_activity_raw
+ *
+ * Set-returning function dispatched with EXECUTE ON ALL SEGMENTS (which
+ * Cloudberry only permits for set-returning functions). Each segment emits a
+ * single row: the raw JSON response body from its local PXF instance, scoped
to
+ * that segment. The pxf_stat_activity view unions these rows and expands them
+ * into typed columns.
+ */
+Datum
+pxf_stat_activity_raw(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+
+ funcctx = SRF_FIRSTCALL_INIT();
+
+ /* the fetched body must outlive this call, so build it in the
+ * multi-call context and hand it back one row at a time */
+ oldcontext =
MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ funcctx->user_fctx = fetch_stat_activity_body();
+ funcctx->max_calls = 1;
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+
+ if (funcctx->call_cntr < funcctx->max_calls)
+ SRF_RETURN_NEXT(funcctx, PointerGetDatum((text *)
funcctx->user_fctx));
+
+ SRF_RETURN_DONE(funcctx);
+}
diff --git
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/activity/ActiveRequest.java
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/activity/ActiveRequest.java
new file mode 100644
index 00000000..c1d0bf40
--- /dev/null
+++
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/activity/ActiveRequest.java
@@ -0,0 +1,167 @@
+package org.apache.cloudberry.pxf.service.activity;
+
+/*
+ * 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.
+ */
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.cloudberry.pxf.service.bridge.Bridge;
+
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A single in-flight PXF request tracked by {@link ActiveRequestRegistry}.
+ * <p>
+ * In addition to the immutable {@link ActiveRequestInfo} snapshot (which
defines
+ * the `pxf_stat_activity` JSON wire contract) it holds the two live
+ * handles used to terminate the request from another thread:
+ * - the worker `thread` that is processing it (for `pxf_interrupt_backend`)
+ * - the current `bridge` (for `pxf_cancel_backend`)
+ */
+@Slf4j
+class ActiveRequest {
+
+ /*
+ * Concurrency: PXF processes a request synchronously on a servlet
+ * thread that is returned to a pool afterwards, so a naive
+ * `thread.interrupt()` could land on an unrelated request the thread has
+ * since picked up. To prevent that, every mutation that races with request
+ * completion is serialized on `lock` and gated by the `finished`
+ * flag:
+ * <ul>
+ * <li>the worker calls `markFinished()` in its `finally` block
+ * before it can return to the pool;</li>
+ * <li>`cancelIfActive()` / `interruptIfActive()` only act while
+ * `finished == false`, which — because both sides take the same
+ * lock — guarantees the worker is still executing this request.</li>
+ * </ul>
+ */
+
+ /** Immutable snapshot exposed via {@code pxf_stat_activity}. */
+ final ActiveRequestInfo info;
+
+ /** The servlet worker thread that is processing this request. */
+ private final Thread thread;
+
+ /** Serializes cancellation/interruption against request completion. */
+ private final ReentrantLock lock = new ReentrantLock();
+
+ /** Set once the request has completed; guarded by {@link #lock}. */
+ private boolean finished;
+
+ /** Set when the request has been asked to stop; read by the worker loop.
*/
+ private volatile boolean cancelled;
+
+ /** The bridge currently in use by the worker; guarded by {@link #lock}. */
+ private Bridge bridge;
+
+ ActiveRequest(ActiveRequestInfo info, Thread thread) {
+ this.info = info;
+ this.thread = thread;
+ }
+
+ /**
+ * @return whether this request has been asked to stop; polled by the
worker
+ * loop so it can break out between fragments/records without waiting for a
+ * blocking read to be unblocked by {@link #cancelIfActive()}.
+ */
+ boolean isCancelled() {
+ return cancelled;
+ }
+
+ /**
+ * Records (or clears, with {@code null}) the bridge the worker is
currently
+ * iterating over, so that {@link #cancelIfActive()} can end it.
+ */
+ void setBridge(Bridge bridge) {
+ lock.lock();
+ try {
+ this.bridge = bridge;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Marks the request as completed. After this returns, {@link
#cancelIfActive()}
+ * and {@link #interruptIfActive()} become no-ops, so the worker thread
can be
+ * safely returned to the pool without risking a stray interrupt.
+ */
+ void markFinished() {
+ lock.lock();
+ try {
+ finished = true;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Requests cancellation of this request if it is still active: raises the
+ * {@link #cancelled} flag and ends the current bridge to unblock a pending
+ * read. Safe to call concurrently with the worker's own bridge lifecycle;
+ * {@code endIteration()} must therefore be idempotent enough to tolerate a
+ * double close (its error is logged and swallowed).
+ *
+ * @return {@code true} if the request was still active and was signalled
+ */
+ boolean cancelIfActive() {
+ lock.lock();
+ try {
+ if (finished) {
+ return false;
+ }
+ cancelled = true;
+ if (bridge != null) {
+ try {
+ bridge.endIteration();
+ } catch (Exception e) {
+ log.warn("Ignoring error while cancelling bridge for {}",
describe(), e);
+ }
+ }
+ return true;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Interrupts the worker thread if the request is still active. Holding the
+ * lock while checking {@link #finished} guarantees the thread has not yet
+ * returned to the pool, so the interrupt cannot leak onto a later request.
+ *
+ * @return {@code true} if the request was still active and was interrupted
+ */
+ boolean interruptIfActive() {
+ lock.lock();
+ try {
+ if (finished) {
+ return false;
+ }
+ thread.interrupt();
+ return true;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private String describe() {
+ return String.format("session %d, segment %d, xid %s",
+ info.getGpSessionId(), info.getSegmentId(),
info.getTransactionId());
+ }
+}
diff --git
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/activity/ActiveRequestInfo.java
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/activity/ActiveRequestInfo.java
new file mode 100644
index 00000000..38fdcfc2
--- /dev/null
+++
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/activity/ActiveRequestInfo.java
@@ -0,0 +1,96 @@
+package org.apache.cloudberry.pxf.service.activity;
+
+/*
+ * 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.
+ */
+
+import lombok.Getter;
+import org.apache.cloudberry.pxf.api.model.RequestContext;
+
+/**
+ * Immutable snapshot of an in-flight PXF request, captured at the moment the
+ * request started being processed. Instances are exposed via the
+ * `/pxf/stat_activity` endpoint.
+ * <p>
+ * The getters are serialized to JSON by Jackson.
+ */
+@Getter
+public class ActiveRequestInfo {
+
+ /** The Cloudberry segment id that originated the request. */
+ private final int segmentId;
+
+ /** The Cloudberry session id (aka ssid). */
+ private final int gpSessionId;
+
+ /** The Cloudberry command count within the session (aka ccnt). */
+ private final int gpCommandCount;
+
+ /** The Cloudberry transaction id (XID) of the originating query. */
+ private final String transactionId;
+
+ /** The kind of operation: READ_BRIDGE or WRITE_BRIDGE. */
+ private final String requestType;
+
+ /** The end-user identity that issued the request. */
+ private final String user;
+
+ /** The name of the PXF server configuration used by the request. */
+ private final String serverName;
+
+ /** The profile associated with the request (e.g. hdfs:text). */
+ private final String profile;
+
+ /** The originating Cloudberry schema name. */
+ private final String schemaName;
+
+ /** The originating Cloudberry table name. */
+ private final String tableName;
+
+ /** The data source (file path or external resource identifier). */
+ private final String dataSource;
+
+ /** Epoch milliseconds when the request started being processed. */
+ private final long startTimeMs;
+
+ /** The hostname of the PXF instance that is serving the request. */
+ private final String host;
+
+ /**
+ * Captures a snapshot of the relevant fields from the request context.
+ *
+ * @param context the request context of the in-flight request
+ * @param startTimeMs epoch milliseconds when processing started
+ * @param host hostname of the PXF instance serving the request
+ */
+ public ActiveRequestInfo(RequestContext context, long startTimeMs, String
host) {
+ this.segmentId = context.getSegmentId();
+ this.gpSessionId = context.getGpSessionId();
+ this.gpCommandCount = context.getGpCommandCount();
+ this.transactionId = context.getTransactionId();
+ this.requestType = context.getRequestType() == null ? null :
context.getRequestType().name();
+ this.user = context.getUser();
+ this.serverName = context.getServerName();
+ this.profile = context.getProfile();
+ this.schemaName = context.getSchemaName();
+ this.tableName = context.getTableName();
+ this.dataSource = context.getDataSource();
+ this.startTimeMs = startTimeMs;
+ this.host = host;
+ }
+}
diff --git
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/activity/ActiveRequestRegistry.java
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/activity/ActiveRequestRegistry.java
new file mode 100644
index 00000000..22f7d2f7
--- /dev/null
+++
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/activity/ActiveRequestRegistry.java
@@ -0,0 +1,191 @@
+package org.apache.cloudberry.pxf.service.activity;
+
+/*
+ * 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.
+ */
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.cloudberry.pxf.api.model.RequestContext;
+import org.apache.cloudberry.pxf.service.bridge.Bridge;
+import org.springframework.stereotype.Component;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Registry of the requests that are currently being processed by this PXF
instance.
+ * A request is registered when it starts being processed by
+ * `BaseServiceImpl.processData()` and unregistered when it ends.
+ */
+@Component
+@Slf4j
+public class ActiveRequestRegistry {
+
+ /** Sentinel meaning "all segments" for the segment-id filter. */
+ public static final int ALL_SEGMENTS = -1;
+
+ /** All in-flight requests on this instance. */
+ private final Set<ActiveRequest> activeRequests =
ConcurrentHashMap.newKeySet();
+
+ /**
+ * The request currently being processed by the calling worker thread, used
+ * by the worker itself `attachBridge`, `isCurrentCancelled`) and to locate
+ * the entry to remove on `unregister` without threading a handle through
the
+ * processing call chain. Set on `register` and cleared on `unregister`.
+ */
+ private final ThreadLocal<ActiveRequest> currentRequest = new
ThreadLocal<>();
+
+ private final String hostname;
+
+ public ActiveRequestRegistry() {
+ this.hostname = resolveHostname();
+ }
+
+ /**
+ * Registers the request being processed by the calling thread as
in-flight.
+ * Must be paired with a call to `unregister()` in a finally block, on the
+ * same thread.
+ *
+ * @param context the request context of the request being processed
+ */
+ public void register(RequestContext context) {
+ ActiveRequestInfo info = new ActiveRequestInfo(context,
System.currentTimeMillis(), hostname);
+ ActiveRequest request = new ActiveRequest(info,
Thread.currentThread());
+ activeRequests.add(request);
+ currentRequest.set(request);
+ }
+
+ /**
+ * Removes the request registered by the calling thread from the registry
and
+ * marks it finished so that any concurrent `cancel()` / `interrupt` can no
+ * longer act on the (soon to be recycled) worker thread. No-op if the
calling
+ * thread has no registered request.
+ */
+ public void unregister() {
+ ActiveRequest request = currentRequest.get();
+ if (request != null) {
+ activeRequests.remove(request);
+ request.markFinished();
+ }
+ currentRequest.remove();
+ }
+
+ /**
+ * Records the bridge the calling worker thread is currently iterating
over,
+ * so that a concurrent `cancel` can end it. Pass `null` to clear
+ * the reference once the bridge is closed. No-op if the caller is not a
+ * registered worker thread.
+ *
+ * @param bridge the bridge in use, or `null` to detach
+ */
+ public void attachBridge(Bridge bridge) {
+ ActiveRequest request = currentRequest.get();
+ if (request != null) {
+ request.setBridge(bridge);
+ }
+ }
+
+ /**
+ * @return whether the request being processed by the calling worker thread
+ * has been asked to cancel. Polled by the read/write loops so they stop
+ * between fragments/records.
+ */
+ public boolean isCurrentCancelled() {
+ ActiveRequest request = currentRequest.get();
+ return request != null && request.isCancelled();
+ }
+
+ /**
+ * Returns a snapshot of the currently active requests, optionally filtered
+ * by the originating segment id.
+ *
+ * @param segmentId the originating segment id to filter by, or
+ * `ALL_SEGMENTS` to return activity for all segments
+ * @return list of active request descriptors
+ */
+ public List<ActiveRequestInfo> snapshot(int segmentId) {
+ List<ActiveRequestInfo> result = new
ArrayList<>(activeRequests.size());
+ for (ActiveRequest request : activeRequests) {
+ if (matchesSegment(request, segmentId)) {
+ result.add(request.info);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Gracefully cancels every active request of the given Cloudberry session
on
+ * this segment by ending its current bridge (see
`ActiveRequest.cancelIfActive()`).
+ *
+ * @param segmentId the originating segment id, or `ALL_SEGMENTS`
+ * @param sessionId the Cloudberry session id whose requests to cancel
+ * @return the number of active requests that were signalled
+ */
+ public int cancel(int segmentId, int sessionId) {
+ int count = 0;
+ for (ActiveRequest request : activeRequests) {
+ if (matchesSegment(request, segmentId) &&
request.info.getGpSessionId() == sessionId) {
+ if (request.cancelIfActive()) {
+ count++;
+ }
+ }
+ }
+ log.info("pxf_cancel_backend: signalled {} request(s) for session {}
on segment {}",
+ count, sessionId, segmentId);
+ return count;
+ }
+
+ /**
+ * Interrupts the worker thread of every active request of the given
+ * Cloudberry session on this segment (see
`ActiveRequest.interruptIfActive()`).
+ *
+ * @param segmentId the originating segment id, or `ALL_SEGMENTS`
+ * @param sessionId the Cloudberry session id whose requests to interrupt
+ * @return the number of active requests that were interrupted
+ */
+ public int interrupt(int segmentId, int sessionId) {
+ int count = 0;
+ for (ActiveRequest request : activeRequests) {
+ if (matchesSegment(request, segmentId) &&
request.info.getGpSessionId() == sessionId) {
+ if (request.interruptIfActive()) {
+ count++;
+ }
+ }
+ }
+ log.info("pxf_interrupt_backend: interrupted {} request(s) for session
{} on segment {}",
+ count, sessionId, segmentId);
+ return count;
+ }
+
+ private static boolean matchesSegment(ActiveRequest request, int
segmentId) {
+ return segmentId == ALL_SEGMENTS || request.info.getSegmentId() ==
segmentId;
+ }
+
+ private static String resolveHostname() {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ log.warn("Unable to resolve local hostname for pxf_stat_activity
reporting", e);
+ return "unknown";
+ }
+ }
+}
diff --git
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/BaseServiceImpl.java
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/BaseServiceImpl.java
index fb61d91b..fdb9c0f6 100644
---
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/BaseServiceImpl.java
+++
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/BaseServiceImpl.java
@@ -6,6 +6,7 @@ import org.apache.cloudberry.pxf.api.model.ConfigurationFactory;
import org.apache.cloudberry.pxf.api.model.RequestContext;
import org.apache.cloudberry.pxf.api.utilities.Utilities;
import org.apache.cloudberry.pxf.service.MetricsReporter;
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestRegistry;
import org.apache.cloudberry.pxf.service.bridge.Bridge;
import org.apache.cloudberry.pxf.service.bridge.BridgeFactory;
import org.apache.cloudberry.pxf.service.security.SecurityService;
@@ -26,26 +27,30 @@ public abstract class BaseServiceImpl<T> extends
PxfErrorReporter<T> {
private final ConfigurationFactory configurationFactory;
private final BridgeFactory bridgeFactory;
private final SecurityService securityService;
+ private final ActiveRequestRegistry activeRequestRegistry;
/**
* Creates a new instance of the service with auto-wired dependencies.
*
- * @param serviceName name of the service
- * @param configurationFactory configuration factory
- * @param bridgeFactory bridge factory
- * @param securityService security service
- * @param metricsReporter metrics reporter service
+ * @param serviceName name of the service
+ * @param configurationFactory configuration factory
+ * @param bridgeFactory bridge factory
+ * @param securityService security service
+ * @param metricsReporter metrics reporter service
+ * @param activeRequestRegistry registry of in-flight requests
*/
protected BaseServiceImpl(String serviceName,
ConfigurationFactory configurationFactory,
BridgeFactory bridgeFactory,
SecurityService securityService,
- MetricsReporter metricsReporter) {
+ MetricsReporter metricsReporter,
+ ActiveRequestRegistry activeRequestRegistry) {
this.serviceName = serviceName;
this.configurationFactory = configurationFactory;
this.bridgeFactory = bridgeFactory;
this.securityService = securityService;
this.metricsReporter = metricsReporter;
+ this.activeRequestRegistry = activeRequestRegistry;
}
/**
@@ -70,8 +75,19 @@ public abstract class BaseServiceImpl<T> extends
PxfErrorReporter<T> {
Instant startTime = Instant.now();
- // execute processing action with a proper identity
- OperationResult result = securityService.doAs(context, action);
+ // clear any interrupt status left on this pooled worker thread by a
+ // previous request that was targeted by pxf_interrupt_backend, so it
+ // cannot leak into the request we are about to process
+ Thread.interrupted();
+
+ activeRequestRegistry.register(context);
+ OperationResult result;
+ try {
+ // execute processing action with a proper identity
+ result = securityService.doAs(context, action);
+ } finally {
+ activeRequestRegistry.unregister();
+ }
// obtain results after executing the action
OperationStats stats = result.getStats();
@@ -115,4 +131,24 @@ public abstract class BaseServiceImpl<T> extends
PxfErrorReporter<T> {
protected Bridge getBridge(RequestContext context) {
return bridgeFactory.getBridge(context);
}
+
+ /**
+ * Records the bridge the current worker thread is iterating over so that a
+ * concurrent `pxf_cancel_backend` can end it. Pass `null` to
+ * detach once the bridge is closed.
+ *
+ * @param bridge the bridge in use, or {@code null} to detach
+ */
+ protected void attachBridge(Bridge bridge) {
+ activeRequestRegistry.attachBridge(bridge);
+ }
+
+ /**
+ * @return whether the request currently processed by this thread has been
+ * asked to cancel via `pxf_cancel_backend`; the read/write loops poll
+ * this so they stop between fragments/records.
+ */
+ protected boolean isCancelled() {
+ return activeRequestRegistry.isCurrentCancelled();
+ }
}
diff --git
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/ReadServiceImpl.java
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/ReadServiceImpl.java
index e4558afe..d03a53e0 100644
---
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/ReadServiceImpl.java
+++
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/ReadServiceImpl.java
@@ -3,6 +3,7 @@ package org.apache.cloudberry.pxf.service.controller;
import com.google.common.io.CountingOutputStream;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
+import org.apache.cloudberry.pxf.api.error.PxfRuntimeException;
import org.apache.cloudberry.pxf.api.io.Writable;
import org.apache.cloudberry.pxf.api.model.ConfigurationFactory;
import org.apache.cloudberry.pxf.api.model.Fragment;
@@ -11,6 +12,7 @@ import org.apache.cloudberry.pxf.api.model.RequestContext;
import org.apache.cloudberry.pxf.api.utilities.Utilities;
import org.apache.cloudberry.pxf.service.FragmenterService;
import org.apache.cloudberry.pxf.service.MetricsReporter;
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestRegistry;
import org.apache.cloudberry.pxf.service.bridge.Bridge;
import org.apache.cloudberry.pxf.service.bridge.BridgeFactory;
import org.apache.cloudberry.pxf.service.security.SecurityService;
@@ -35,18 +37,20 @@ public class ReadServiceImpl extends
BaseServiceImpl<OperationStats> implements
/**
* Creates a new instance.
*
- * @param configurationFactory configuration factory
- * @param bridgeFactory bridge factory
- * @param securityService security service
- * @param fragmenterService fragmenter service
- * @param metricsReporter metrics reporter service
+ * @param configurationFactory configuration factory
+ * @param bridgeFactory bridge factory
+ * @param securityService security service
+ * @param fragmenterService fragmenter service
+ * @param metricsReporter metrics reporter service
+ * @param activeRequestRegistry registry of in-flight requests
*/
public ReadServiceImpl(ConfigurationFactory configurationFactory,
BridgeFactory bridgeFactory,
SecurityService securityService,
FragmenterService fragmenterService,
- MetricsReporter metricsReporter) {
- super("Read", configurationFactory, bridgeFactory, securityService,
metricsReporter);
+ MetricsReporter metricsReporter,
+ ActiveRequestRegistry activeRequestRegistry) {
+ super("Read", configurationFactory, bridgeFactory, securityService,
metricsReporter, activeRequestRegistry);
this.fragmenterService = fragmenterService;
}
@@ -84,6 +88,13 @@ public class ReadServiceImpl extends
BaseServiceImpl<OperationStats> implements
try {
List<Fragment> fragments =
fragmenterService.getFragmentsForSegment(context);
for (int i = 0; i < fragments.size(); i++) {
+ // stop before starting the next fragment if the request was
+ // cancelled via pxf_cancel_backend
+ if (isCancelled()) {
+ throw new PxfRuntimeException(String.format(
+ "Read of resource %s cancelled by
pxf_cancel_backend after %d of %d fragments",
+ context.getDataSource(), i, fragments.size()));
+ }
Fragment fragment = fragments.get(i);
sourceName = fragment.getSourceName();
String profile = fragment.getProfile();
@@ -148,6 +159,8 @@ public class ReadServiceImpl extends
BaseServiceImpl<OperationStats> implements
Bridge bridge = null;
try {
bridge = getBridge(context);
+ // expose the bridge so pxf_cancel_backend can end it mid-read
+ attachBridge(bridge);
if (!bridge.beginIteration()) {
log.debug("Skipping streaming fragment {} of resource {}",
context.getFragmentIndex(), context.getDataSource());
@@ -167,6 +180,9 @@ public class ReadServiceImpl extends
BaseServiceImpl<OperationStats> implements
bridge.endIteration();
} catch (Exception e) {
log.warn("Ignoring error encountered during
bridge.endIteration()", e);
+ } finally {
+ // stop exposing the now-closed bridge to
pxf_cancel_backend
+ attachBridge(null);
}
}
Duration duration = Duration.between(startTime, Instant.now());
diff --git
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/WriteServiceImpl.java
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/WriteServiceImpl.java
index f11278e9..4317522c 100644
---
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/WriteServiceImpl.java
+++
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/controller/WriteServiceImpl.java
@@ -2,10 +2,12 @@ package org.apache.cloudberry.pxf.service.controller;
import com.google.common.io.CountingInputStream;
import lombok.extern.slf4j.Slf4j;
+import org.apache.cloudberry.pxf.api.error.PxfRuntimeException;
import org.apache.cloudberry.pxf.api.model.ConfigurationFactory;
import org.apache.cloudberry.pxf.api.model.RequestContext;
import org.apache.cloudberry.pxf.api.utilities.Utilities;
import org.apache.cloudberry.pxf.service.MetricsReporter;
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestRegistry;
import org.apache.cloudberry.pxf.service.bridge.Bridge;
import org.apache.cloudberry.pxf.service.bridge.BridgeFactory;
import org.apache.cloudberry.pxf.service.security.SecurityService;
@@ -24,15 +26,18 @@ public class WriteServiceImpl extends
BaseServiceImpl<OperationStats> implements
/**
* Creates a new instance.
*
- * @param configurationFactory configuration factory
- * @param bridgeFactory bridge factory
- * @param securityService security service
+ * @param configurationFactory configuration factory
+ * @param bridgeFactory bridge factory
+ * @param securityService security service
+ * @param metricsReporter metrics reporter service
+ * @param activeRequestRegistry registry of in-flight requests
*/
public WriteServiceImpl(ConfigurationFactory configurationFactory,
BridgeFactory bridgeFactory,
SecurityService securityService,
- MetricsReporter metricsReporter) {
- super("Write", configurationFactory, bridgeFactory, securityService,
metricsReporter);
+ MetricsReporter metricsReporter,
+ ActiveRequestRegistry activeRequestRegistry) {
+ super("Write", configurationFactory, bridgeFactory, securityService,
metricsReporter, activeRequestRegistry);
}
@Override
@@ -62,11 +67,18 @@ public class WriteServiceImpl extends
BaseServiceImpl<OperationStats> implements
// dataStream (and inputStream as the result) will close automatically
at the end of the try block
CountingInputStream countingInputStream = new
CountingInputStream(inputStream);
try (DataInputStream dataStream = new
DataInputStream(countingInputStream)) {
+ // expose the bridge so pxf_cancel_backend can end it mid-write
+ attachBridge(bridge);
// open the output file, returns true or throws an error
bridge.beginIteration();
- while (bridge.setNext(dataStream)) {
+ while (!isCancelled() && bridge.setNext(dataStream)) {
operationStats.reportCompletedRecord(countingInputStream.getCount());
}
+ if (isCancelled()) {
+ throw new PxfRuntimeException(String.format(
+ "Write to resource %s cancelled by pxf_cancel_backend",
+ context.getDataSource()));
+ }
} catch (Exception e) {
operationResult.setException(e);
} finally {
@@ -76,6 +88,9 @@ public class WriteServiceImpl extends
BaseServiceImpl<OperationStats> implements
if (operationResult.getException() == null) {
operationResult.setException(e);
}
+ } finally {
+ // stop exposing the now-closed bridge to pxf_cancel_backend
+ attachBridge(null);
}
// in the case where we fail to report a record due to an
exception,
diff --git
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/rest/PxfBackendControlResource.java
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/rest/PxfBackendControlResource.java
new file mode 100644
index 00000000..c44d0c01
--- /dev/null
+++
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/rest/PxfBackendControlResource.java
@@ -0,0 +1,83 @@
+package org.apache.cloudberry.pxf.service.rest;
+
+/*
+ * 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.
+ */
+
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestRegistry;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * REST endpoints that terminate in-flight PXF requests, backing the
+ * `pxf_cancel_backend` and `pxf_interrupt_backend` SQL functions.
+ */
+@RestController
+@RequestMapping("/pxf")
+public class PxfBackendControlResource {
+
+ private static final String SEGMENT_ID_HEADER = "X-GP-SEGMENT-ID";
+ private static final String SESSION_ID_HEADER = "X-GP-SESSION-ID";
+
+ private final ActiveRequestRegistry activeRequestRegistry;
+
+ public PxfBackendControlResource(ActiveRequestRegistry
activeRequestRegistry) {
+ this.activeRequestRegistry = activeRequestRegistry;
+ }
+
+ /**
+ * Gracefully cancels the active requests of the given session on this
segment
+ * by ending their current bridge.
+ *
+ * @param sessionId the Cloudberry session id whose requests to cancel
+ * @param segmentId the originating segment id; when absent, all segments
+ * @return a JSON object `{"cancelled":N}` with the number signalled
+ */
+ @GetMapping(value = "/cancel_backend", produces =
MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity<Map<String, Integer>> cancelBackend(
+ @RequestHeader(SESSION_ID_HEADER) int sessionId,
+ @RequestHeader(value = SEGMENT_ID_HEADER, required = false)
Integer segmentId) {
+ int filter = (segmentId == null) ? ActiveRequestRegistry.ALL_SEGMENTS
: segmentId;
+ int cancelled = activeRequestRegistry.cancel(filter, sessionId);
+ return ResponseEntity.ok(Collections.singletonMap("cancelled",
cancelled));
+ }
+
+ /**
+ * Interrupts the worker thread(s) of the active requests of the given
session
+ * on this segment.
+ *
+ * @param sessionId the Cloudberry session id whose requests to interrupt
+ * @param segmentId the originating segment id; when absent, all segments
+ * @return a JSON object `{"interrupted":N}` with the number interrupted
+ */
+ @GetMapping(value = "/interrupt_backend", produces =
MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity<Map<String, Integer>> interruptBackend(
+ @RequestHeader(SESSION_ID_HEADER) int sessionId,
+ @RequestHeader(value = SEGMENT_ID_HEADER, required = false)
Integer segmentId) {
+ int filter = (segmentId == null) ? ActiveRequestRegistry.ALL_SEGMENTS
: segmentId;
+ int interrupted = activeRequestRegistry.interrupt(filter, sessionId);
+ return ResponseEntity.ok(Collections.singletonMap("interrupted",
interrupted));
+ }
+}
diff --git
a/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/rest/PxfStatActivityResource.java
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/rest/PxfStatActivityResource.java
new file mode 100644
index 00000000..94c1c032
--- /dev/null
+++
b/server/pxf-service/src/main/java/org/apache/cloudberry/pxf/service/rest/PxfStatActivityResource.java
@@ -0,0 +1,66 @@
+package org.apache.cloudberry.pxf.service.rest;
+
+/*
+ * 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.
+ */
+
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestInfo;
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestRegistry;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * REST endpoint that exposes the requests currently being processed by this
PXF
+ * instance. It backs the `pxf_stat_activity` SQL view.
+ */
+@RestController
+@RequestMapping("/pxf")
+public class PxfStatActivityResource {
+
+ private static final String SEGMENT_ID_HEADER = "X-GP-SEGMENT-ID";
+
+ private final ActiveRequestRegistry activeRequestRegistry;
+
+ public PxfStatActivityResource(ActiveRequestRegistry
activeRequestRegistry) {
+ this.activeRequestRegistry = activeRequestRegistry;
+ }
+
+ /**
+ * Returns the active requests known to this PXF instance, optionally
+ * filtered by the originating segment id supplied in the request header.
+ *
+ * @param segmentId the originating segment id; when absent, activity for
all
+ * segments is returned
+ * @return a JSON object `{"activities":[...]}`
+ */
+ @GetMapping(value = "/stat_activity", produces =
MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity<Map<String, List<ActiveRequestInfo>>> statActivity(
+ @RequestHeader(value = SEGMENT_ID_HEADER, required = false)
Integer segmentId) {
+ int filter = (segmentId == null) ? ActiveRequestRegistry.ALL_SEGMENTS
: segmentId;
+ List<ActiveRequestInfo> activities =
activeRequestRegistry.snapshot(filter);
+ return ResponseEntity.ok(Collections.singletonMap("activities",
activities));
+ }
+}
diff --git
a/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/activity/ActiveRequestRegistryTest.java
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/activity/ActiveRequestRegistryTest.java
new file mode 100644
index 00000000..2b949692
--- /dev/null
+++
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/activity/ActiveRequestRegistryTest.java
@@ -0,0 +1,250 @@
+package org.apache.cloudberry.pxf.service.activity;
+
+/*
+ * 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.
+ */
+
+import org.apache.cloudberry.pxf.api.io.Writable;
+import org.apache.cloudberry.pxf.api.model.RequestContext;
+import org.apache.cloudberry.pxf.service.bridge.Bridge;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.DataInputStream;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ActiveRequestRegistryTest {
+
+ private ActiveRequestRegistry registry;
+
+ @BeforeEach
+ public void setup() {
+ registry = new ActiveRequestRegistry();
+ }
+
+ private RequestContext context(int segmentId, String xid) {
+ return context(segmentId, xid, 0);
+ }
+
+ private RequestContext context(int segmentId, String xid, int sessionId) {
+ RequestContext context = new RequestContext();
+ context.setRequestType(RequestContext.RequestType.READ_BRIDGE);
+ context.setSegmentId(segmentId);
+ context.setTransactionId(xid);
+ context.setGpSessionId(sessionId);
+ return context;
+ }
+
+ @Test
+ public void emptyRegistryReturnsNoActivity() {
+
assertTrue(registry.snapshot(ActiveRequestRegistry.ALL_SEGMENTS).isEmpty());
+ assertTrue(registry.snapshot(0).isEmpty());
+ }
+
+ @Test
+ public void registeredRequestIsVisibleThenRemovedOnUnregister() {
+ registry.register(context(3, "100"));
+
+ List<ActiveRequestInfo> all =
registry.snapshot(ActiveRequestRegistry.ALL_SEGMENTS);
+ assertEquals(1, all.size());
+ assertEquals(3, all.get(0).getSegmentId());
+ assertEquals("100", all.get(0).getTransactionId());
+ assertEquals("READ_BRIDGE", all.get(0).getRequestType());
+
+ registry.unregister();
+
assertTrue(registry.snapshot(ActiveRequestRegistry.ALL_SEGMENTS).isEmpty());
+ }
+
+ @Test
+ public void snapshotFiltersBySegmentId() {
+ registry.register(context(0, "100"));
+ registry.register(context(1, "101"));
+ registry.register(context(1, "102"));
+
+ assertEquals(3,
registry.snapshot(ActiveRequestRegistry.ALL_SEGMENTS).size());
+ assertEquals(1, registry.snapshot(0).size());
+ assertEquals(2, registry.snapshot(1).size());
+ assertTrue(registry.snapshot(2).isEmpty());
+ }
+
+ @Test
+ public void concurrentRequestsTrackedIndependently() throws Exception {
+ // Each request registers/unregisters on its own thread, as the real
+ // servlet workers do; entries are tracked by ActiveRequest identity.
+ Worker w1 = new Worker(context(0, "100"), null);
+ Worker w2 = new Worker(context(0, "101"), null);
+ w1.start();
+ w2.start();
+
+ assertEquals(2, registry.snapshot(0).size());
+ w1.finishAndJoin();
+ assertEquals(1, registry.snapshot(0).size());
+ w2.finishAndJoin();
+ assertTrue(registry.snapshot(0).isEmpty());
+ }
+
+ @Test
+ public void cancelEndsBridgeAndRaisesCancelledFlagForMatchingSession()
throws Exception {
+ CountingBridge bridge = new CountingBridge();
+ Worker worker = new Worker(context(0, "100", 42), bridge);
+ worker.start();
+
+ assertEquals(1, registry.cancel(0, 42));
+ assertEquals(1, bridge.endIterationCount.get());
+
+ worker.finishAndJoin();
+ assertTrue(worker.cancelledSeen, "worker should observe the cancelled
flag via isCurrentCancelled()");
+ assertFalse(worker.interrupted, "cancel must not interrupt the worker
thread");
+ }
+
+ @Test
+ public void cancelIgnoresOtherSessionsAndSegments() throws Exception {
+ CountingBridge bridge = new CountingBridge();
+ Worker worker = new Worker(context(0, "100", 42), bridge);
+ worker.start();
+
+ assertEquals(0, registry.cancel(0, 99), "different session");
+ assertEquals(0, registry.cancel(1, 42), "different segment");
+ assertEquals(0, bridge.endIterationCount.get());
+
+ worker.finishAndJoin();
+ }
+
+ @Test
+ public void interruptWakesMatchingWorkerThread() throws Exception {
+ Worker worker = new Worker(context(0, "100", 42), null);
+ worker.start();
+
+ assertEquals(1, registry.interrupt(0, 42));
+
+ // interrupting unblocks the worker's interruptible wait on its own
+ worker.join(5000);
+ assertFalse(worker.isAlive(), "interrupted worker should have
finished");
+ assertTrue(worker.interrupted, "worker thread should have been
interrupted");
+ }
+
+ @Test
+ public void interruptIgnoresOtherSessions() throws Exception {
+ Worker worker = new Worker(context(0, "100", 42), null);
+ worker.start();
+
+ assertEquals(0, registry.interrupt(0, 99));
+
+ worker.finishAndJoin();
+ assertFalse(worker.interrupted);
+ }
+
+ @Test
+ public void cancelAndInterruptAreNoOpsAfterUnregister() {
+ // register and unregister synchronously on this thread so the entry is
+ // marked finished; both operations must then refuse to act
+ registry.register(context(0, "100", 42));
+ registry.unregister();
+
+ assertEquals(0, registry.cancel(0, 42));
+ assertEquals(0, registry.interrupt(0, 42));
+ assertFalse(Thread.currentThread().isInterrupted(), "the test thread
must not be interrupted");
+ }
+
+ /**
+ * A worker that registers itself (capturing its own thread, as the real
+ * servlet worker does), optionally attaches a bridge, then blocks on an
+ * interruptible latch until released or interrupted, and finally
unregisters.
+ */
+ private final class Worker extends Thread {
+ private final RequestContext context;
+ private final Bridge bridge;
+ private final CountDownLatch registered = new CountDownLatch(1);
+ private final CountDownLatch release = new CountDownLatch(1);
+ volatile boolean interrupted;
+ volatile boolean cancelledSeen;
+
+ Worker(RequestContext context, Bridge bridge) {
+ this.context = context;
+ this.bridge = bridge;
+ }
+
+ @Override
+ public void run() {
+ registry.register(context);
+ if (bridge != null) {
+ registry.attachBridge(bridge);
+ }
+ registered.countDown();
+ try {
+ release.await();
+ } catch (InterruptedException e) {
+ interrupted = true;
+ } finally {
+ cancelledSeen = registry.isCurrentCancelled();
+ registry.unregister();
+ }
+ }
+
+ @Override
+ public synchronized void start() {
+ super.start();
+ try {
+ assertTrue(registered.await(5, TimeUnit.SECONDS), "worker
failed to register");
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void finishAndJoin() throws InterruptedException {
+ release.countDown();
+ join(5000);
+ assertFalse(isAlive(), "worker should have finished");
+ }
+ }
+
+ /** Minimal bridge that only counts endIteration() invocations. */
+ private static final class CountingBridge implements Bridge {
+ final AtomicInteger endIterationCount = new AtomicInteger();
+ final AtomicBoolean began = new AtomicBoolean();
+
+ @Override
+ public boolean beginIteration() {
+ began.set(true);
+ return true;
+ }
+
+ @Override
+ public Writable getNext() {
+ return null;
+ }
+
+ @Override
+ public boolean setNext(DataInputStream inputStream) {
+ return false;
+ }
+
+ @Override
+ public void endIteration() {
+ endIterationCount.incrementAndGet();
+ }
+ }
+}
diff --git
a/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/controller/ReadServiceImplTest.java
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/controller/ReadServiceImplTest.java
index d4ba09d0..6f3699ff 100644
---
a/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/controller/ReadServiceImplTest.java
+++
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/controller/ReadServiceImplTest.java
@@ -8,6 +8,7 @@ import org.apache.cloudberry.pxf.api.model.Fragment;
import org.apache.cloudberry.pxf.api.model.RequestContext;
import org.apache.cloudberry.pxf.service.FragmenterService;
import org.apache.cloudberry.pxf.service.MetricsReporter;
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestRegistry;
import org.apache.cloudberry.pxf.service.bridge.Bridge;
import org.apache.cloudberry.pxf.service.bridge.BridgeFactory;
import org.apache.cloudberry.pxf.service.security.SecurityService;
@@ -75,7 +76,7 @@ public class ReadServiceImplTest {
return result;
});
- readService = new ReadServiceImpl(mockConfigurationFactory,
mockBridgeFactory, mockSecurityService, mockFragmenterService,
mockMetricReporter);
+ readService = new ReadServiceImpl(mockConfigurationFactory,
mockBridgeFactory, mockSecurityService, mockFragmenterService,
mockMetricReporter, new ActiveRequestRegistry());
}
@Test
diff --git
a/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/controller/WriteServiceImplTest.java
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/controller/WriteServiceImplTest.java
index 0c5cdbc7..031eab80 100644
---
a/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/controller/WriteServiceImplTest.java
+++
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/controller/WriteServiceImplTest.java
@@ -4,6 +4,7 @@ import org.apache.hadoop.conf.Configuration;
import org.apache.cloudberry.pxf.api.model.ConfigurationFactory;
import org.apache.cloudberry.pxf.api.model.RequestContext;
import org.apache.cloudberry.pxf.service.MetricsReporter;
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestRegistry;
import org.apache.cloudberry.pxf.service.bridge.Bridge;
import org.apache.cloudberry.pxf.service.bridge.BridgeFactory;
import org.apache.cloudberry.pxf.service.security.SecurityService;
@@ -61,7 +62,7 @@ public class WriteServiceImplTest {
});
when(mockBridgeFactory.getBridge(mockContext)).thenReturn(mockBridge);
- writeService = new WriteServiceImpl(mockConfigurationFactory,
mockBridgeFactory, mockSecurityService, mockMetricReporter);
+ writeService = new WriteServiceImpl(mockConfigurationFactory,
mockBridgeFactory, mockSecurityService, mockMetricReporter, new
ActiveRequestRegistry());
}
@Test
diff --git
a/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/rest/PxfBackendControlResourceIT.java
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/rest/PxfBackendControlResourceIT.java
new file mode 100644
index 00000000..5cc99db2
--- /dev/null
+++
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/rest/PxfBackendControlResourceIT.java
@@ -0,0 +1,92 @@
+package org.apache.cloudberry.pxf.service.rest;
+
+/*
+ * 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.
+ */
+
+import org.apache.cloudberry.pxf.service.HttpHeaderDecoder;
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestRegistry;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(PxfBackendControlResource.class)
+public class PxfBackendControlResourceIT {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @MockBean
+ private ActiveRequestRegistry mockRegistry;
+
+ // required by web-layer beans pulled into the @WebMvcTest slice
+ @MockBean
+ private HttpHeaderDecoder mockHttpHeaderDecoder;
+
+ @Test
+ public void cancelBackendFiltersBySegmentAndSessionHeaders() throws
Exception {
+ when(mockRegistry.cancel(eq(3), eq(42))).thenReturn(2);
+
+ mvc.perform(get("/pxf/cancel_backend")
+ .header("X-GP-SEGMENT-ID", "3")
+ .header("X-GP-SESSION-ID", "42"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.cancelled").value(2));
+
+ verify(mockRegistry).cancel(3, 42);
+ }
+
+ @Test
+ public void cancelBackendWithoutSegmentHeaderTargetsAllSegments() throws
Exception {
+ when(mockRegistry.cancel(eq(ActiveRequestRegistry.ALL_SEGMENTS),
eq(42))).thenReturn(1);
+
+ mvc.perform(get("/pxf/cancel_backend").header("X-GP-SESSION-ID", "42"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.cancelled").value(1));
+
+ verify(mockRegistry).cancel(ActiveRequestRegistry.ALL_SEGMENTS, 42);
+ }
+
+ @Test
+ public void interruptBackendFiltersBySegmentAndSessionHeaders() throws
Exception {
+ when(mockRegistry.interrupt(eq(3), eq(42))).thenReturn(1);
+
+ mvc.perform(get("/pxf/interrupt_backend")
+ .header("X-GP-SEGMENT-ID", "3")
+ .header("X-GP-SESSION-ID", "42"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.interrupted").value(1));
+
+ verify(mockRegistry).interrupt(3, 42);
+ }
+
+ @Test
+ public void missingSessionHeaderIsBadRequest() throws Exception {
+ mvc.perform(get("/pxf/cancel_backend").header("X-GP-SEGMENT-ID", "3"))
+ .andExpect(status().isBadRequest());
+ }
+}
diff --git
a/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/rest/PxfStatActivityResourceIT.java
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/rest/PxfStatActivityResourceIT.java
new file mode 100644
index 00000000..20dd38cf
--- /dev/null
+++
b/server/pxf-service/src/test/java/org/apache/cloudberry/pxf/service/rest/PxfStatActivityResourceIT.java
@@ -0,0 +1,89 @@
+package org.apache.cloudberry.pxf.service.rest;
+
+/*
+ * 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.
+ */
+
+import org.apache.cloudberry.pxf.api.model.RequestContext;
+import org.apache.cloudberry.pxf.service.HttpHeaderDecoder;
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestInfo;
+import org.apache.cloudberry.pxf.service.activity.ActiveRequestRegistry;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.Collections;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(PxfStatActivityResource.class)
+public class PxfStatActivityResourceIT {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @MockBean
+ private ActiveRequestRegistry mockRegistry;
+
+ @MockBean
+ private HttpHeaderDecoder mockHttpHeaderDecoder;
+
+ private static ActiveRequestInfo sampleInfo(int segmentId, String xid) {
+ RequestContext context = new RequestContext();
+ context.setRequestType(RequestContext.RequestType.READ_BRIDGE);
+ context.setSegmentId(segmentId);
+ context.setTransactionId(xid);
+ context.setGpSessionId(42);
+ context.setGpCommandCount(7);
+ context.setProfile("hdfs:text");
+ return new ActiveRequestInfo(context, 1718700000000L, "sdw3");
+ }
+
+ @Test
+ public void emptyRegistryReturnsEmptyActivities() throws Exception {
+ when(mockRegistry.snapshot(ActiveRequestRegistry.ALL_SEGMENTS))
+ .thenReturn(Collections.emptyList());
+
+ mvc.perform(get("/pxf/stat_activity"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.activities").isArray())
+ .andExpect(jsonPath("$.activities").isEmpty());
+ }
+
+ @Test
+ public void filtersBySegmentIdHeaderAndSerializesFields() throws Exception
{
+ when(mockRegistry.snapshot(eq(3)))
+ .thenReturn(Collections.singletonList(sampleInfo(3, "1234")));
+
+ mvc.perform(get("/pxf/stat_activity").header("X-GP-SEGMENT-ID", "3"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.activities[0].segmentId").value(3))
+
.andExpect(jsonPath("$.activities[0].transactionId").value("1234"))
+
.andExpect(jsonPath("$.activities[0].requestType").value("READ_BRIDGE"))
+ .andExpect(jsonPath("$.activities[0].gpSessionId").value(42))
+
.andExpect(jsonPath("$.activities[0].profile").value("hdfs:text"))
+ .andExpect(jsonPath("$.activities[0].host").value("sdw3"))
+
.andExpect(jsonPath("$.activities[0].startTimeMs").value(1718700000000L));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]