From f48ae14544f4e148b6e7700b9d8c9a63014c3865 Mon Sep 17 00:00:00 2001 From: panish16 Date: Tue, 26 May 2026 16:38:49 -0700 Subject: [PATCH 01/15] fix: add pg8000 graceful shutdown on Cloud Run scale-down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calls setup_pg8000_close_event_listener(engine) after db init in all lear queue services and legal-api. Scoped to long-lived Cloud Run services only — GCP jobs excluded as no pg8000 errors observed in logs. Resolves #33564 --- legal-api/src/legal_api/__init__.py | 4 +++- queue_services/business-bn/src/business_bn/__init__.py | 6 +++++- .../src/business_digital_credentials/__init__.py | 4 +++- .../business-emailer/src/business_emailer/__init__.py | 6 +++++- .../business-filer/src/business_filer/__init__.py | 6 +++++- queue_services/business-pay/src/business_pay/__init__.py | 6 +++++- 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index a105b69f8c..20022276e2 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -73,7 +73,7 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: app.config.from_object(config.CONFIGURATION[run_mode]) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig + from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener db_config = DBConfig( instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], database=app.config.get("DB_NAME", ""), @@ -94,6 +94,8 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: with app.app_context(): # db require app context digital_credentials.init_app(app) + if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover + setup_pg8000_close_event_listener(db.engine) cache.init_app(app) diff --git a/queue_services/business-bn/src/business_bn/__init__.py b/queue_services/business-bn/src/business_bn/__init__.py index 7c500d8532..55f4e434de 100644 --- a/queue_services/business-bn/src/business_bn/__init__.py +++ b/queue_services/business-bn/src/business_bn/__init__.py @@ -58,7 +58,7 @@ def create_app(environment: str = os.getenv("DEPLOYMENT_ENV", "production"), **k app.config.from_object(CONFIGURATION[environment]) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig + from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener db_config = DBConfig( instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], database=app.config.get("DB_NAME", ""), @@ -70,6 +70,10 @@ def create_app(environment: str = os.getenv("DEPLOYMENT_ENV", "production"), **k app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() db.init_app(app) + + if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover + with app.app_context(): + setup_pg8000_close_event_listener(db.engine) register_endpoints(app) gcp_queue.init_app(app) diff --git a/queue_services/business-digital-credentials/src/business_digital_credentials/__init__.py b/queue_services/business-digital-credentials/src/business_digital_credentials/__init__.py index b75a298b18..6097fb14e5 100644 --- a/queue_services/business-digital-credentials/src/business_digital_credentials/__init__.py +++ b/queue_services/business-digital-credentials/src/business_digital_credentials/__init__.py @@ -18,7 +18,7 @@ import os from business_registry_digital_credentials import digital_credentials -from cloud_sql_connector import DBConfig +from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener from flask import Flask from business_model.models.db import db @@ -67,5 +67,7 @@ def create_app( with app.app_context(): digital_credentials.init_app(app) + if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover + setup_pg8000_close_event_listener(db.engine) return app diff --git a/queue_services/business-emailer/src/business_emailer/__init__.py b/queue_services/business-emailer/src/business_emailer/__init__.py index d659d31d6d..0c574f7d01 100644 --- a/queue_services/business-emailer/src/business_emailer/__init__.py +++ b/queue_services/business-emailer/src/business_emailer/__init__.py @@ -36,7 +36,7 @@ def create_app(environment: Config = ProdConfig, **kwargs) -> Flask: flags.init_app(app) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig + from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener db_config = DBConfig( instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], database=app.config.get("DB_NAME", ""), @@ -48,6 +48,10 @@ def create_app(environment: Config = ProdConfig, **kwargs) -> Flask: app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() db.init_app(app) + + if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover + with app.app_context(): + setup_pg8000_close_event_listener(db.engine) register_endpoints(app) gcp_queue.init_app(app) diff --git a/queue_services/business-filer/src/business_filer/__init__.py b/queue_services/business-filer/src/business_filer/__init__.py index d024e13e78..a79600eb78 100644 --- a/queue_services/business-filer/src/business_filer/__init__.py +++ b/queue_services/business-filer/src/business_filer/__init__.py @@ -61,7 +61,7 @@ def create_app(environment: str = os.getenv("DEPLOYMENT_ENV", "production"), **k flags.init_app(app, kwargs.get("ld_test_data")) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig + from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener db_config = DBConfig( instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], database=app.config.get("DB_NAME", ""), @@ -73,6 +73,10 @@ def create_app(environment: str = os.getenv("DEPLOYMENT_ENV", "production"), **k app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() db.init_app(app) + + if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover + with app.app_context(): + setup_pg8000_close_event_listener(db.engine) gcp_queue.init_app(app) register_endpoints(app) diff --git a/queue_services/business-pay/src/business_pay/__init__.py b/queue_services/business-pay/src/business_pay/__init__.py index 85fe9702f4..9f3d055c76 100644 --- a/queue_services/business-pay/src/business_pay/__init__.py +++ b/queue_services/business-pay/src/business_pay/__init__.py @@ -56,7 +56,7 @@ def create_app(environment: Config = ProdConfig, **kwargs) -> Flask: flags.init_app(app) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig + from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener db_config = DBConfig( instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], @@ -69,6 +69,10 @@ def create_app(environment: Config = ProdConfig, **kwargs) -> Flask: app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() db.init_app(app) + + if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover + with app.app_context(): + setup_pg8000_close_event_listener(db.engine) register_endpoints(app) gcp_queue.init_app(app) From 00a1f4ec307f181e2d923ee846fc780a4a7dc49c Mon Sep 17 00:00:00 2001 From: panish16 Date: Wed, 27 May 2026 11:47:11 -0700 Subject: [PATCH 02/15] fix: bump cloud-sql-connector lock to v0.2.3 and use inline pg8000 shutdown for legal-api --- legal-api/src/legal_api/__init__.py | 29 +++++++++++++++++-- queue_services/business-bn/poetry.lock | 4 +-- .../business-digital-credentials/poetry.lock | 4 +-- queue_services/business-emailer/poetry.lock | 4 +-- queue_services/business-filer/poetry.lock | 4 +-- queue_services/business-pay/poetry.lock | 4 +-- 6 files changed, 37 insertions(+), 12 deletions(-) diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index 20022276e2..68111a398f 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -61,6 +61,31 @@ setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first +def _setup_pg8000_graceful_shutdown(engine) -> None: + """Suppress pg8000 InterfaceError on connection close during Cloud Run scale-down.""" + import logging + from sqlalchemy import event + try: + from pg8000.exceptions import InterfaceError as _InterfaceError + except ImportError: + _InterfaceError = None + + @event.listens_for(engine, "connect") + def on_connect(dbapi_conn, _connection_record): + orig_close = dbapi_conn.close + + def safe_close(): + try: + orig_close() + except Exception as exc: + if _InterfaceError and isinstance(exc, _InterfaceError): + logging.getLogger(__name__).debug("Suppressed pg8000 InterfaceError on teardown.") + else: + raise + + dbapi_conn.close = safe_close + + def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: """Return a configured Flask App using the Factory method.""" run_mode = run_mode or os.getenv("RUN_MODE", "production") @@ -73,7 +98,7 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: app.config.from_object(config.CONFIGURATION[run_mode]) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener + from cloud_sql_connector import DBConfig db_config = DBConfig( instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], database=app.config.get("DB_NAME", ""), @@ -95,7 +120,7 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: with app.app_context(): # db require app context digital_credentials.init_app(app) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - setup_pg8000_close_event_listener(db.engine) + _setup_pg8000_graceful_shutdown(db.engine) cache.init_app(app) diff --git a/queue_services/business-bn/poetry.lock b/queue_services/business-bn/poetry.lock index 5f1bf67be9..1c4c69be20 100644 --- a/queue_services/business-bn/poetry.lock +++ b/queue_services/business-bn/poetry.lock @@ -639,7 +639,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloud-sql-connector" -version = "0.2.1" +version = "0.2.3" description = "Cloud SQL connection utilities for database connectivity with authentication and schema management" optional = false python-versions = "^3.12" @@ -656,7 +656,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "2a3d5a2d5b5ff3c905626f3193db5ed0af6d4952" +resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/queue_services/business-digital-credentials/poetry.lock b/queue_services/business-digital-credentials/poetry.lock index dbaaeefc31..7f8fd4db7b 100644 --- a/queue_services/business-digital-credentials/poetry.lock +++ b/queue_services/business-digital-credentials/poetry.lock @@ -649,7 +649,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloud-sql-connector" -version = "0.2.1" +version = "0.2.3" description = "Cloud SQL connection utilities for database connectivity with authentication and schema management" optional = false python-versions = "^3.12" @@ -666,7 +666,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "2a3d5a2d5b5ff3c905626f3193db5ed0af6d4952" +resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/queue_services/business-emailer/poetry.lock b/queue_services/business-emailer/poetry.lock index fef1ae30ca..4654a94a2f 100644 --- a/queue_services/business-emailer/poetry.lock +++ b/queue_services/business-emailer/poetry.lock @@ -691,7 +691,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloud-sql-connector" -version = "0.2.1" +version = "0.2.3" description = "Cloud SQL connection utilities for database connectivity with authentication and schema management" optional = false python-versions = "^3.12" @@ -708,7 +708,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "2a3d5a2d5b5ff3c905626f3193db5ed0af6d4952" +resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/queue_services/business-filer/poetry.lock b/queue_services/business-filer/poetry.lock index a269280dd7..5e9f44f43d 100644 --- a/queue_services/business-filer/poetry.lock +++ b/queue_services/business-filer/poetry.lock @@ -585,7 +585,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloud-sql-connector" -version = "0.2.1" +version = "0.2.3" description = "Cloud SQL connection utilities for database connectivity with authentication and schema management" optional = false python-versions = "^3.12" @@ -602,7 +602,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "2a3d5a2d5b5ff3c905626f3193db5ed0af6d4952" +resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/queue_services/business-pay/poetry.lock b/queue_services/business-pay/poetry.lock index 69a5cd6f6a..8e5eda8070 100644 --- a/queue_services/business-pay/poetry.lock +++ b/queue_services/business-pay/poetry.lock @@ -545,7 +545,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloud-sql-connector" -version = "0.2.1" +version = "0.2.3" description = "Cloud SQL connection utilities for database connectivity with authentication and schema management" optional = false python-versions = "^3.12" @@ -562,7 +562,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "2a3d5a2d5b5ff3c905626f3193db5ed0af6d4952" +resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" subdirectory = "python/cloud-sql-connector" [[package]] From fbe862ebac152228d0b924a926182d43557103cb Mon Sep 17 00:00:00 2001 From: panish16 Date: Wed, 27 May 2026 12:02:30 -0700 Subject: [PATCH 03/15] fix: correct cloud-sql-connector lock to v0.2.3 with correct remote hash --- queue_services/business-bn/poetry.lock | 2 +- queue_services/business-digital-credentials/poetry.lock | 2 +- queue_services/business-emailer/poetry.lock | 2 +- queue_services/business-filer/poetry.lock | 2 +- queue_services/business-pay/poetry.lock | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/queue_services/business-bn/poetry.lock b/queue_services/business-bn/poetry.lock index 1c4c69be20..ecef663964 100644 --- a/queue_services/business-bn/poetry.lock +++ b/queue_services/business-bn/poetry.lock @@ -656,7 +656,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" +resolved_reference = "9a760cf5ea31d3f95806c2dcac15ca9d9800df51" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/queue_services/business-digital-credentials/poetry.lock b/queue_services/business-digital-credentials/poetry.lock index 7f8fd4db7b..ebae117277 100644 --- a/queue_services/business-digital-credentials/poetry.lock +++ b/queue_services/business-digital-credentials/poetry.lock @@ -666,7 +666,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" +resolved_reference = "9a760cf5ea31d3f95806c2dcac15ca9d9800df51" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/queue_services/business-emailer/poetry.lock b/queue_services/business-emailer/poetry.lock index 4654a94a2f..4f0b3da4ab 100644 --- a/queue_services/business-emailer/poetry.lock +++ b/queue_services/business-emailer/poetry.lock @@ -708,7 +708,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" +resolved_reference = "9a760cf5ea31d3f95806c2dcac15ca9d9800df51" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/queue_services/business-filer/poetry.lock b/queue_services/business-filer/poetry.lock index 5e9f44f43d..ce48c59470 100644 --- a/queue_services/business-filer/poetry.lock +++ b/queue_services/business-filer/poetry.lock @@ -602,7 +602,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" +resolved_reference = "9a760cf5ea31d3f95806c2dcac15ca9d9800df51" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/queue_services/business-pay/poetry.lock b/queue_services/business-pay/poetry.lock index 8e5eda8070..55a3fbf618 100644 --- a/queue_services/business-pay/poetry.lock +++ b/queue_services/business-pay/poetry.lock @@ -562,7 +562,7 @@ sqlalchemy = "^2.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "b83281702031386f92a0f3d27577e15b842b769b" +resolved_reference = "9a760cf5ea31d3f95806c2dcac15ca9d9800df51" subdirectory = "python/cloud-sql-connector" [[package]] From 1e9d07ac8a61572f4a8c4d0abe783f7bec279aee Mon Sep 17 00:00:00 2001 From: panish16 Date: Thu, 28 May 2026 14:16:09 -0700 Subject: [PATCH 04/15] fix: move imports to module level and rename _InterfaceError in legal-api --- legal-api/src/legal_api/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index 68111a398f..247fdb6d08 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -35,12 +35,14 @@ This module is the API for the Legal Entity system. """ +import logging import os from typing import Optional from flask import Flask, jsonify from registry_schemas import __version__ as registry_schemas_version from registry_schemas.flask import SchemaServices +from sqlalchemy import event from legal_api import config, models from legal_api.models import db @@ -63,12 +65,10 @@ def _setup_pg8000_graceful_shutdown(engine) -> None: """Suppress pg8000 InterfaceError on connection close during Cloud Run scale-down.""" - import logging - from sqlalchemy import event try: - from pg8000.exceptions import InterfaceError as _InterfaceError + from pg8000.exceptions import InterfaceError as _interface_error except ImportError: - _InterfaceError = None + _interface_error = None @event.listens_for(engine, "connect") def on_connect(dbapi_conn, _connection_record): @@ -78,7 +78,7 @@ def safe_close(): try: orig_close() except Exception as exc: - if _InterfaceError and isinstance(exc, _InterfaceError): + if _interface_error and isinstance(exc, _interface_error): logging.getLogger(__name__).debug("Suppressed pg8000 InterfaceError on teardown.") else: raise From 31b3db1d70ca50509d90721ce2005bff1c0f505c Mon Sep 17 00:00:00 2001 From: panish16 Date: Thu, 28 May 2026 14:31:42 -0700 Subject: [PATCH 05/15] fix: suppress N813 on pg8000 InterfaceError import alias in legal-api --- legal-api/src/legal_api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index 247fdb6d08..f59627b648 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -66,7 +66,7 @@ def _setup_pg8000_graceful_shutdown(engine) -> None: """Suppress pg8000 InterfaceError on connection close during Cloud Run scale-down.""" try: - from pg8000.exceptions import InterfaceError as _interface_error + from pg8000.exceptions import InterfaceError as _interface_error # noqa: N813 except ImportError: _interface_error = None From 37c3fcff21427ba45d7b2d89381031fe06e3e0f9 Mon Sep 17 00:00:00 2001 From: panish16 Date: Tue, 2 Jun 2026 09:28:56 -0700 Subject: [PATCH 06/15] fix: simplify queue service pg8000 setup, remove unnecessary DBConfig blocks Remove the DBConfig blocks from all 5 queue services (business-bn, business-emailer, business-pay, business-filer, business-digital-credentials). These services connect via SQLALCHEMY_DATABASE_URI in their configs and don't need Cloud SQL Connector connection pooling. The setup_pg8000_close_event_listener call is now unconditional since the function already returns early for non-pg8000 engines. --- .../business-bn/src/business_bn/__init__.py | 18 +++--------------- .../business_digital_credentials/__init__.py | 16 ++-------------- .../src/business_emailer/__init__.py | 18 +++--------------- .../src/business_filer/__init__.py | 18 +++--------------- .../business-pay/src/business_pay/__init__.py | 19 +++---------------- 5 files changed, 14 insertions(+), 75 deletions(-) diff --git a/queue_services/business-bn/src/business_bn/__init__.py b/queue_services/business-bn/src/business_bn/__init__.py index 55f4e434de..faba9cd39b 100644 --- a/queue_services/business-bn/src/business_bn/__init__.py +++ b/queue_services/business-bn/src/business_bn/__init__.py @@ -36,6 +36,7 @@ """ import os +from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask from business_model.models import db @@ -57,23 +58,10 @@ def create_app(environment: str = os.getenv("DEPLOYMENT_ENV", "production"), **k app.logger = StructuredLogging(app).get_logger() app.config.from_object(CONFIGURATION[environment]) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener - db_config = DBConfig( - instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], - database=app.config.get("DB_NAME", ""), - user=app.config.get("DB_USER", ""), - ip_type=app.config["DB_IP_TYPE"], - pool_recycle=60, - schema="public", - ) - app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() - db.init_app(app) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - with app.app_context(): - setup_pg8000_close_event_listener(db.engine) + with app.app_context(): + setup_pg8000_close_event_listener(db.engine) register_endpoints(app) gcp_queue.init_app(app) diff --git a/queue_services/business-digital-credentials/src/business_digital_credentials/__init__.py b/queue_services/business-digital-credentials/src/business_digital_credentials/__init__.py index 6097fb14e5..abc4fbf579 100644 --- a/queue_services/business-digital-credentials/src/business_digital_credentials/__init__.py +++ b/queue_services/business-digital-credentials/src/business_digital_credentials/__init__.py @@ -18,7 +18,7 @@ import os from business_registry_digital_credentials import digital_credentials -from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener +from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask from business_model.models.db import db @@ -50,24 +50,12 @@ def create_app( if app.config.get("LD_SDK_KEY", None): flags.init_app(app) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - db_config = DBConfig( - instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], - database=app.config.get("DB_NAME", ""), - user=app.config.get("DB_USER", ""), - ip_type=app.config["DB_IP_TYPE"], - pool_recycle=60, - schema="public", - ) - app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() - db.init_app(app) register_endpoints(app) gcp_queue.init_app(app) with app.app_context(): digital_credentials.init_app(app) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - setup_pg8000_close_event_listener(db.engine) + setup_pg8000_close_event_listener(db.engine) return app diff --git a/queue_services/business-emailer/src/business_emailer/__init__.py b/queue_services/business-emailer/src/business_emailer/__init__.py index 0c574f7d01..59f6345b1b 100644 --- a/queue_services/business-emailer/src/business_emailer/__init__.py +++ b/queue_services/business-emailer/src/business_emailer/__init__.py @@ -15,6 +15,7 @@ This module is the service worker for sending emails about entity related events. """ +from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask from business_model.models.db import db @@ -35,23 +36,10 @@ def create_app(environment: Config = ProdConfig, **kwargs) -> Flask: if app.config.get("LD_SDK_KEY", None): flags.init_app(app) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener - db_config = DBConfig( - instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], - database=app.config.get("DB_NAME", ""), - user=app.config.get("DB_USER", ""), - ip_type=app.config["DB_IP_TYPE"], - pool_recycle=60, - schema="public", - ) - app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() - db.init_app(app) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - with app.app_context(): - setup_pg8000_close_event_listener(db.engine) + with app.app_context(): + setup_pg8000_close_event_listener(db.engine) register_endpoints(app) gcp_queue.init_app(app) diff --git a/queue_services/business-filer/src/business_filer/__init__.py b/queue_services/business-filer/src/business_filer/__init__.py index a79600eb78..bbea6503fa 100644 --- a/queue_services/business-filer/src/business_filer/__init__.py +++ b/queue_services/business-filer/src/business_filer/__init__.py @@ -35,6 +35,7 @@ import os from business_model.models import db +from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask from business_filer.config import DevConfig, ProdConfig, TestConfig @@ -60,23 +61,10 @@ def create_app(environment: str = os.getenv("DEPLOYMENT_ENV", "production"), **k app.logger = StructuredLogging(app).get_logger() flags.init_app(app, kwargs.get("ld_test_data")) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener - db_config = DBConfig( - instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], - database=app.config.get("DB_NAME", ""), - user=app.config.get("DB_USER", ""), - ip_type=app.config["DB_IP_TYPE"], - pool_recycle=60, - schema="public", - ) - app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() - db.init_app(app) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - with app.app_context(): - setup_pg8000_close_event_listener(db.engine) + with app.app_context(): + setup_pg8000_close_event_listener(db.engine) gcp_queue.init_app(app) register_endpoints(app) diff --git a/queue_services/business-pay/src/business_pay/__init__.py b/queue_services/business-pay/src/business_pay/__init__.py index 9f3d055c76..676cbaddb8 100644 --- a/queue_services/business-pay/src/business_pay/__init__.py +++ b/queue_services/business-pay/src/business_pay/__init__.py @@ -38,6 +38,7 @@ """ from __future__ import annotations +from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask from .config import Config, ProdConfig @@ -55,24 +56,10 @@ def create_app(environment: Config = ProdConfig, **kwargs) -> Flask: if app.config.get("LD_SDK_KEY", None): flags.init_app(app) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig, setup_pg8000_close_event_listener - - db_config = DBConfig( - instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], - database=app.config.get("DB_NAME", ""), - user=app.config.get("DB_USER", ""), - ip_type=app.config["DB_IP_TYPE"], - pool_recycle=60, - schema="public", - ) - app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() - db.init_app(app) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - with app.app_context(): - setup_pg8000_close_event_listener(db.engine) + with app.app_context(): + setup_pg8000_close_event_listener(db.engine) register_endpoints(app) gcp_queue.init_app(app) From 2f1deed72d57a0a646296ba4d9b2ea020b8a31c3 Mon Sep 17 00:00:00 2001 From: panish16 Date: Tue, 2 Jun 2026 10:14:48 -0700 Subject: [PATCH 07/15] fix: move DBConfig engine options to config.py and use shared pg8000 listener DBConfig/SQLALCHEMY_ENGINE_OPTIONS setup was removed from __init__.py without being moved anywhere, breaking Cloud SQL connections on GCP. Move it into each service's config.py alongside the existing CLOUDSQL_INSTANCE_CONNECTION_NAME block. Replace legal-api's local _setup_pg8000_graceful_shutdown reimplementation with setup_pg8000_close_event_listener from cloud_sql_connector. --- legal-api/src/legal_api/__init__.py | 40 +------------------ legal-api/src/legal_api/config.py | 10 +++++ .../business-bn/src/business_bn/config.py | 10 +++++ .../business_digital_credentials/config.py | 10 +++++ .../src/business_emailer/config.py | 10 +++++ .../src/business_filer/config.py | 10 +++++ .../business-pay/src/business_pay/config.py | 10 +++++ 7 files changed, 62 insertions(+), 38 deletions(-) diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index f59627b648..ac9ec34245 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -35,14 +35,13 @@ This module is the API for the Legal Entity system. """ -import logging import os from typing import Optional +from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask, jsonify from registry_schemas import __version__ as registry_schemas_version from registry_schemas.flask import SchemaServices -from sqlalchemy import event from legal_api import config, models from legal_api.models import db @@ -63,29 +62,6 @@ setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first -def _setup_pg8000_graceful_shutdown(engine) -> None: - """Suppress pg8000 InterfaceError on connection close during Cloud Run scale-down.""" - try: - from pg8000.exceptions import InterfaceError as _interface_error # noqa: N813 - except ImportError: - _interface_error = None - - @event.listens_for(engine, "connect") - def on_connect(dbapi_conn, _connection_record): - orig_close = dbapi_conn.close - - def safe_close(): - try: - orig_close() - except Exception as exc: - if _interface_error and isinstance(exc, _interface_error): - logging.getLogger(__name__).debug("Suppressed pg8000 InterfaceError on teardown.") - else: - raise - - dbapi_conn.close = safe_close - - def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: """Return a configured Flask App using the Factory method.""" run_mode = run_mode or os.getenv("RUN_MODE", "production") @@ -97,18 +73,6 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: else: app.config.from_object(config.CONFIGURATION[run_mode]) - if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - from cloud_sql_connector import DBConfig - db_config = DBConfig( - instance_name=app.config["CLOUDSQL_INSTANCE_CONNECTION_NAME"], - database=app.config.get("DB_NAME", ""), - user=app.config.get("DB_USER", ""), - ip_type=app.config["DB_IP_TYPE"], - pool_recycle = 60, - schema="public", - ) - app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.get_engine_options() - app.logger = StructuredLogging(app).get_logger() init_db(app) rsbc_schemas.init_app(app) @@ -120,7 +84,7 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: with app.app_context(): # db require app context digital_credentials.init_app(app) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - _setup_pg8000_graceful_shutdown(db.engine) + setup_pg8000_close_event_listener(db.engine) cache.init_app(app) diff --git a/legal-api/src/legal_api/config.py b/legal-api/src/legal_api/config.py index 124cfbf689..5cd7f3ef23 100644 --- a/legal-api/src/legal_api/config.py +++ b/legal-api/src/legal_api/config.py @@ -23,6 +23,7 @@ import os import sys +from cloud_sql_connector import DBConfig from dotenv import find_dotenv, load_dotenv # this will load all the envars from a .env file located in the project root (api) @@ -118,6 +119,15 @@ class _Config: # pylint: disable=too-few-public-methods SQLALCHEMY_DATABASE_URI = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" elif CLOUDSQL_INSTANCE_CONNECTION_NAME: SQLALCHEMY_DATABASE_URI = "postgresql+pg8000://" + db_config = DBConfig( + instance_name=CLOUDSQL_INSTANCE_CONNECTION_NAME, + database=DB_NAME, + user=DB_USER, + ip_type=DB_IP_TYPE, + pool_recycle=60, + schema="public", + ) + SQLALCHEMY_ENGINE_OPTIONS = db_config.get_engine_options() # JWT_OIDC Settings JWT_OIDC_WELL_KNOWN_CONFIG = os.getenv("JWT_OIDC_WELL_KNOWN_CONFIG") diff --git a/queue_services/business-bn/src/business_bn/config.py b/queue_services/business-bn/src/business_bn/config.py index 2c261bb7df..138c517d1f 100644 --- a/queue_services/business-bn/src/business_bn/config.py +++ b/queue_services/business-bn/src/business_bn/config.py @@ -42,6 +42,7 @@ import os +from cloud_sql_connector import DBConfig from dotenv import find_dotenv, load_dotenv # this will load all the envars from a .env file located in the project root (api) @@ -93,6 +94,15 @@ class Config: # pylint: disable=too-few-public-methods SQLALCHEMY_DATABASE_URI = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" elif CLOUDSQL_INSTANCE_CONNECTION_NAME: SQLALCHEMY_DATABASE_URI = "postgresql+pg8000://" + db_config = DBConfig( + instance_name=CLOUDSQL_INSTANCE_CONNECTION_NAME, + database=DB_NAME, + user=DB_USER, + ip_type=DB_IP_TYPE, + pool_recycle=60, + schema="public", + ) + SQLALCHEMY_ENGINE_OPTIONS = db_config.get_engine_options() # legislative timezone for future effective dating LEGISLATIVE_TIMEZONE = os.getenv("LEGISLATIVE_TIMEZONE", "America/Vancouver") diff --git a/queue_services/business-digital-credentials/src/business_digital_credentials/config.py b/queue_services/business-digital-credentials/src/business_digital_credentials/config.py index 6c4137950f..c19e3c8eca 100644 --- a/queue_services/business-digital-credentials/src/business_digital_credentials/config.py +++ b/queue_services/business-digital-credentials/src/business_digital_credentials/config.py @@ -22,6 +22,7 @@ import os +from cloud_sql_connector import DBConfig from dotenv import find_dotenv, load_dotenv # this will load all the envars from a .env file located in the project root (api) @@ -80,6 +81,15 @@ class Config: # pylint: disable=too-few-public-methods SQLALCHEMY_DATABASE_URI = f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" elif CLOUDSQL_INSTANCE_CONNECTION_NAME: SQLALCHEMY_DATABASE_URI = "postgresql+pg8000://" + db_config = DBConfig( + instance_name=CLOUDSQL_INSTANCE_CONNECTION_NAME, + database=DB_NAME, + user=DB_USER, + ip_type=DB_IP_TYPE, + pool_recycle=60, + schema="public", + ) + SQLALCHEMY_ENGINE_OPTIONS = db_config.get_engine_options() # Traction ACA-Py tenant settings to issue credentials from TRACTION_API_URL = os.getenv("TRACTION_API_URL") diff --git a/queue_services/business-emailer/src/business_emailer/config.py b/queue_services/business-emailer/src/business_emailer/config.py index a3a20520f6..7d93384f28 100644 --- a/queue_services/business-emailer/src/business_emailer/config.py +++ b/queue_services/business-emailer/src/business_emailer/config.py @@ -22,6 +22,7 @@ import os +from cloud_sql_connector import DBConfig from dotenv import find_dotenv, load_dotenv # this will load all the envars from a .env file located in the project root (api) @@ -113,6 +114,15 @@ class Config: # pylint: disable=too-few-public-methods SQLALCHEMY_DATABASE_URI = f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}" elif CLOUDSQL_INSTANCE_CONNECTION_NAME: SQLALCHEMY_DATABASE_URI = "postgresql+pg8000://" + db_config = DBConfig( + instance_name=CLOUDSQL_INSTANCE_CONNECTION_NAME, + database=DB_NAME, + user=DB_USER, + ip_type=DB_IP_TYPE, + pool_recycle=60, + schema="public", + ) + SQLALCHEMY_ENGINE_OPTIONS = db_config.get_engine_options() diff --git a/queue_services/business-filer/src/business_filer/config.py b/queue_services/business-filer/src/business_filer/config.py index b4bd30a75f..92c50bbae1 100644 --- a/queue_services/business-filer/src/business_filer/config.py +++ b/queue_services/business-filer/src/business_filer/config.py @@ -22,6 +22,7 @@ import os +from cloud_sql_connector import DBConfig from dotenv import find_dotenv, load_dotenv # this will load all the envars from a .env file located in the project root (api) @@ -54,6 +55,15 @@ class _Config: # pylint: disable=too-few-public-methods SQLALCHEMY_DATABASE_URI = f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" elif CLOUDSQL_INSTANCE_CONNECTION_NAME: SQLALCHEMY_DATABASE_URI = "postgresql+pg8000://" + db_config = DBConfig( + instance_name=CLOUDSQL_INSTANCE_CONNECTION_NAME, + database=DB_NAME, + user=DB_USER, + ip_type=DB_IP_TYPE, + pool_recycle=60, + schema="public", + ) + SQLALCHEMY_ENGINE_OPTIONS = db_config.get_engine_options() COLIN_API = os.getenv("COLIN_API_URL", "") + os.getenv("COLIN_API_VERSION", "") diff --git a/queue_services/business-pay/src/business_pay/config.py b/queue_services/business-pay/src/business_pay/config.py index 9ad4d36a50..62beff3f59 100644 --- a/queue_services/business-pay/src/business_pay/config.py +++ b/queue_services/business-pay/src/business_pay/config.py @@ -43,6 +43,7 @@ import os import random +from cloud_sql_connector import DBConfig from dotenv import find_dotenv, load_dotenv # this will load all the envars from a .env file located in the project root (api) @@ -102,6 +103,15 @@ class Config: # pylint: disable=too-few-public-methods ) elif CLOUDSQL_INSTANCE_CONNECTION_NAME: SQLALCHEMY_DATABASE_URI = "postgresql+pg8000://" + db_config = DBConfig( + instance_name=CLOUDSQL_INSTANCE_CONNECTION_NAME, + database=DB_NAME, + user=DB_USER, + ip_type=DB_IP_TYPE, + pool_recycle=60, + schema="public", + ) + SQLALCHEMY_ENGINE_OPTIONS = db_config.get_engine_options() ENVIRONMENT = os.getenv("DEPLOYMENT_ENV", "production") From e1021dfd1084fe6842b498b2a478d94ae54a4808 Mon Sep 17 00:00:00 2001 From: panish16 Date: Tue, 2 Jun 2026 11:09:34 -0700 Subject: [PATCH 08/15] fix: keep local pg8000 shutdown in legal-api until python3.9 branch is updated setup_pg8000_close_event_listener is not yet available on the cloud-sql-connector-python3.9 branch used by legal-api, so retain the local _setup_pg8000_graceful_shutdown implementation. DBConfig setup remains moved to config.py. --- legal-api/src/legal_api/__init__.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index ac9ec34245..ab83f6dfb2 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -35,13 +35,14 @@ This module is the API for the Legal Entity system. """ +import logging import os from typing import Optional -from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask, jsonify from registry_schemas import __version__ as registry_schemas_version from registry_schemas.flask import SchemaServices +from sqlalchemy import event from legal_api import config, models from legal_api.models import db @@ -62,6 +63,29 @@ setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first +def _setup_pg8000_graceful_shutdown(engine) -> None: + """Suppress pg8000 InterfaceError on connection close during Cloud Run scale-down.""" + try: + from pg8000.exceptions import InterfaceError as _interface_error # noqa: N813 + except ImportError: + _interface_error = None + + @event.listens_for(engine, "connect") + def on_connect(dbapi_conn, _connection_record): + orig_close = dbapi_conn.close + + def safe_close(): + try: + orig_close() + except Exception as exc: + if _interface_error and isinstance(exc, _interface_error): + logging.getLogger(__name__).debug("Suppressed pg8000 InterfaceError on teardown.") + else: + raise + + dbapi_conn.close = safe_close + + def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: """Return a configured Flask App using the Factory method.""" run_mode = run_mode or os.getenv("RUN_MODE", "production") @@ -84,7 +108,7 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: with app.app_context(): # db require app context digital_credentials.init_app(app) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - setup_pg8000_close_event_listener(db.engine) + _setup_pg8000_graceful_shutdown(db.engine) cache.init_app(app) From 25036eebf0ef97ad6eb297bf210fcbf4d9f51574 Mon Sep 17 00:00:00 2001 From: panish16 Date: Tue, 2 Jun 2026 11:20:40 -0700 Subject: [PATCH 09/15] fix: use setup_pg8000_close_event_listener in legal-api and bump lock to 0.2.3 --- legal-api/poetry.lock | 4 ++-- legal-api/src/legal_api/__init__.py | 28 ++-------------------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 826fc233e6..7617e0699d 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -556,7 +556,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloud-sql-connector" -version = "0.2.2" +version = "0.2.3" description = "Cloud SQL connection utilities for database connectivity with authentication and schema management" optional = false python-versions = "^3.9" @@ -573,7 +573,7 @@ sqlalchemy = ">=1.4.0,<3.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "cloud-sql-connector-python3.9" -resolved_reference = "e82cd710c818e55f7b468472b4908e4d14a25a79" +resolved_reference = "c4a2d6b465ecbb0d09695f91c44a1ee6fb0f970e" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index ab83f6dfb2..ac9ec34245 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -35,14 +35,13 @@ This module is the API for the Legal Entity system. """ -import logging import os from typing import Optional +from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask, jsonify from registry_schemas import __version__ as registry_schemas_version from registry_schemas.flask import SchemaServices -from sqlalchemy import event from legal_api import config, models from legal_api.models import db @@ -63,29 +62,6 @@ setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first -def _setup_pg8000_graceful_shutdown(engine) -> None: - """Suppress pg8000 InterfaceError on connection close during Cloud Run scale-down.""" - try: - from pg8000.exceptions import InterfaceError as _interface_error # noqa: N813 - except ImportError: - _interface_error = None - - @event.listens_for(engine, "connect") - def on_connect(dbapi_conn, _connection_record): - orig_close = dbapi_conn.close - - def safe_close(): - try: - orig_close() - except Exception as exc: - if _interface_error and isinstance(exc, _interface_error): - logging.getLogger(__name__).debug("Suppressed pg8000 InterfaceError on teardown.") - else: - raise - - dbapi_conn.close = safe_close - - def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: """Return a configured Flask App using the Factory method.""" run_mode = run_mode or os.getenv("RUN_MODE", "production") @@ -108,7 +84,7 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: with app.app_context(): # db require app context digital_credentials.init_app(app) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - _setup_pg8000_graceful_shutdown(db.engine) + setup_pg8000_close_event_listener(db.engine) cache.init_app(app) From d4bc420c5d288bc12a87abf7e543326de5434262 Mon Sep 17 00:00:00 2001 From: panish16 Date: Tue, 2 Jun 2026 12:16:44 -0700 Subject: [PATCH 10/15] fix: revert legal-api to local pg8000 shutdown until sbc-connect-common PR is merged --- legal-api/poetry.lock | 4 ++-- legal-api/src/legal_api/__init__.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 7617e0699d..826fc233e6 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -556,7 +556,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloud-sql-connector" -version = "0.2.3" +version = "0.2.2" description = "Cloud SQL connection utilities for database connectivity with authentication and schema management" optional = false python-versions = "^3.9" @@ -573,7 +573,7 @@ sqlalchemy = ">=1.4.0,<3.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "cloud-sql-connector-python3.9" -resolved_reference = "c4a2d6b465ecbb0d09695f91c44a1ee6fb0f970e" +resolved_reference = "e82cd710c818e55f7b468472b4908e4d14a25a79" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index ac9ec34245..ab83f6dfb2 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -35,13 +35,14 @@ This module is the API for the Legal Entity system. """ +import logging import os from typing import Optional -from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask, jsonify from registry_schemas import __version__ as registry_schemas_version from registry_schemas.flask import SchemaServices +from sqlalchemy import event from legal_api import config, models from legal_api.models import db @@ -62,6 +63,29 @@ setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first +def _setup_pg8000_graceful_shutdown(engine) -> None: + """Suppress pg8000 InterfaceError on connection close during Cloud Run scale-down.""" + try: + from pg8000.exceptions import InterfaceError as _interface_error # noqa: N813 + except ImportError: + _interface_error = None + + @event.listens_for(engine, "connect") + def on_connect(dbapi_conn, _connection_record): + orig_close = dbapi_conn.close + + def safe_close(): + try: + orig_close() + except Exception as exc: + if _interface_error and isinstance(exc, _interface_error): + logging.getLogger(__name__).debug("Suppressed pg8000 InterfaceError on teardown.") + else: + raise + + dbapi_conn.close = safe_close + + def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: """Return a configured Flask App using the Factory method.""" run_mode = run_mode or os.getenv("RUN_MODE", "production") @@ -84,7 +108,7 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: with app.app_context(): # db require app context digital_credentials.init_app(app) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - setup_pg8000_close_event_listener(db.engine) + _setup_pg8000_graceful_shutdown(db.engine) cache.init_app(app) From 44281cea7ba84bbf0fa13c678f976fbf7e420f4e Mon Sep 17 00:00:00 2001 From: panish16 Date: Tue, 2 Jun 2026 13:34:11 -0700 Subject: [PATCH 11/15] fix: update codecov.yaml with correct business-* flag names and source paths --- codecov.yaml | 94 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/codecov.yaml b/codecov.yaml index 1b80c27959..53788c1c0c 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -13,15 +13,23 @@ coverage: backend: target: auto flags: - - colinapi - - legalapi - - entityemailer - - entityfiler - - entitypay - - entitybn - - entity-digital-credentials - - furnishings - - involuntary-dissolutions + - business-api + - business-bn + - business-bn-retry + - business-digital-credentials + - business-email-reminder + - business-emailer + - business-entity-bn + - business-expired-limited-restoration + - business-filer + - business-filings-notebook-report + - business-furnishings + - business-future-effective-filings + - business-involuntary-dissolutions + - business-pay + - business-registry-model + - business-update-colin-filings + - business-update-legal-filings ignore: - "^/tests/**/*" # ignore test harness code @@ -40,39 +48,71 @@ comment: require_changes: true flags: - colinapi: + business-api: paths: - - colin-api/src/colin_api + - legal-api/src/legal_api carryforward: true - legalapi: + business-bn: paths: - - legal-api/src/legal_api + - queue_services/business-bn/src/business_bn + carryforward: true + business-bn-retry: + paths: + - gcp-jobs/bn-retry/src/bn_retry + carryforward: true + business-digital-credentials: + paths: + - queue_services/business-digital-credentials/src/business_digital_credentials + carryforward: true + business-email-reminder: + paths: + - gcp-jobs/email-reminder/src/email_reminder + carryforward: true + business-emailer: + paths: + - queue_services/business-emailer/src/business_emailer + carryforward: true + business-entity-bn: + paths: + - gcp-jobs/entity-bn/src/entity-bn + carryforward: true + business-expired-limited-restoration: + paths: + - gcp-jobs/expired-limited-restoration/src/expired_limited_restoration + carryforward: true + business-filer: + paths: + - queue_services/business-filer/src/business_filer + carryforward: true + business-filings-notebook-report: + paths: + - gcp-jobs/filings-notebook-report/src/notebookreport carryforward: true - entityemailer: + business-furnishings: paths: - - queue_services/entity-emailer/src/entity_emailer + - gcp-jobs/furnishings/src/furnishings carryforward: true - entityfiler: + business-future-effective-filings: paths: - - queue_services/entity-filer/src/entity_filer + - gcp-jobs/future-effective-filings/src/future_effective_filings carryforward: true - entitypay: + business-involuntary-dissolutions: paths: - - queue_services/entity-pay/src/entity_pay + - gcp-jobs/involuntary-dissolutions/src/involuntary_dissolutions carryforward: true - entitybn: + business-pay: paths: - - queue_services/entity-bn/src/entity_bn + - queue_services/business-pay/src/business_pay carryforward: true - entity-digital-credentials: + business-registry-model: paths: - - queue_services/entity-digital-credentials/src/entity_digital_credentials + - python/common/business-registry-model/src/business_model carryforward: true - furnishings: + business-update-colin-filings: paths: - - jobs/furnishings + - gcp-jobs/update-colin-filings/src/update_colin_filings carryforward: true - involuntary-dissolutions: + business-update-legal-filings: paths: - - jobs/involuntary-dissolutions + - gcp-jobs/update-legal-filings/src/update_legal_filings carryforward: true From 66b34b3fde11cf1aa272f36872109d143c400e7a Mon Sep 17 00:00:00 2001 From: panish16 Date: Tue, 2 Jun 2026 13:38:33 -0700 Subject: [PATCH 12/15] fix: add pytest/coverage config to business-pay, emailer, digital-credentials so coverage.xml is generated and uploaded --- .../pyproject.toml | 44 +++++++++++++++++- .../business-emailer/pyproject.toml | 44 +++++++++++++++++- queue_services/business-pay/pyproject.toml | 45 +++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/queue_services/business-digital-credentials/pyproject.toml b/queue_services/business-digital-credentials/pyproject.toml index d064dd0b41..3540fceb99 100644 --- a/queue_services/business-digital-credentials/pyproject.toml +++ b/queue_services/business-digital-credentials/pyproject.toml @@ -133,7 +133,49 @@ docstring-quotes = "double" "**/__init__.py" = ["F401"] # used for imports [tool.pytest.ini_options] -addopts = "--cov=src" +minversion = "2.0" +testpaths = [ + "tests", +] +addopts = "--verbose --strict -p no:warnings --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml" +python_files = [ + "test*.py" +] +norecursedirs = [ + ".git", ".tox", "venv*", "requirements*", "build", +] +log_cli = true +log_cli_level = "1" +filterwarnings = [ + "ignore::UserWarning" +] +markers = [ + "slow", + "serial", +] + +[tool.coverage.run] +branch = true +source = [ + "src/business_digital_credentials", +] +omit = [ + "wsgi.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "from", + "import", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + 'if __name__ == "__main__":', +] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] diff --git a/queue_services/business-emailer/pyproject.toml b/queue_services/business-emailer/pyproject.toml index e61fd3fae5..467bf73d87 100644 --- a/queue_services/business-emailer/pyproject.toml +++ b/queue_services/business-emailer/pyproject.toml @@ -200,7 +200,49 @@ docstring-quotes = "double" "src/business_emailer/services/namex.py" = ["I001"] # ignoring 'unordered' imports [tool.pytest.ini_options] -addopts = "--cov=src" +minversion = "2.0" +testpaths = [ + "tests", +] +addopts = "--verbose --strict -p no:warnings --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml" +python_files = [ + "test*.py" +] +norecursedirs = [ + ".git", ".tox", "venv*", "requirements*", "build", +] +log_cli = true +log_cli_level = "1" +filterwarnings = [ + "ignore::UserWarning" +] +markers = [ + "slow", + "serial", +] + +[tool.coverage.run] +branch = true +source = [ + "src/business_emailer", +] +omit = [ + "wsgi.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "from", + "import", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + 'if __name__ == "__main__":', +] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] diff --git a/queue_services/business-pay/pyproject.toml b/queue_services/business-pay/pyproject.toml index 7d04048b5d..71170420d8 100644 --- a/queue_services/business-pay/pyproject.toml +++ b/queue_services/business-pay/pyproject.toml @@ -34,6 +34,51 @@ zimports = "^0.6.1" lovely-pytest-docker = "^0.3.1" pytest-cov = "^6.1.1" +[tool.pytest.ini_options] +minversion = "2.0" +testpaths = [ + "tests", +] +addopts = "--verbose --strict -p no:warnings --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml" +python_files = [ + "test*.py" +] +norecursedirs = [ + ".git", ".tox", "venv*", "requirements*", "build", +] +log_cli = true +log_cli_level = "1" +filterwarnings = [ + "ignore::UserWarning" +] +markers = [ + "slow", + "serial", +] + +[tool.coverage.run] +branch = true +source = [ + "src/business_pay", +] +omit = [ + "wsgi.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "from", + "import", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + 'if __name__ == "__main__":', +] + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" From 00a587577801f9d70037519ef1b7f5f4c9f62735 Mon Sep 17 00:00:00 2001 From: panish16 Date: Tue, 2 Jun 2026 15:15:55 -0700 Subject: [PATCH 13/15] revert: restore codecov.yaml to main (unrelated to pg8000 shutdown fix) --- codecov.yaml | 94 +++++++++++++++------------------------------------- 1 file changed, 27 insertions(+), 67 deletions(-) diff --git a/codecov.yaml b/codecov.yaml index 53788c1c0c..1b80c27959 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -13,23 +13,15 @@ coverage: backend: target: auto flags: - - business-api - - business-bn - - business-bn-retry - - business-digital-credentials - - business-email-reminder - - business-emailer - - business-entity-bn - - business-expired-limited-restoration - - business-filer - - business-filings-notebook-report - - business-furnishings - - business-future-effective-filings - - business-involuntary-dissolutions - - business-pay - - business-registry-model - - business-update-colin-filings - - business-update-legal-filings + - colinapi + - legalapi + - entityemailer + - entityfiler + - entitypay + - entitybn + - entity-digital-credentials + - furnishings + - involuntary-dissolutions ignore: - "^/tests/**/*" # ignore test harness code @@ -48,71 +40,39 @@ comment: require_changes: true flags: - business-api: + colinapi: paths: - - legal-api/src/legal_api - carryforward: true - business-bn: - paths: - - queue_services/business-bn/src/business_bn - carryforward: true - business-bn-retry: - paths: - - gcp-jobs/bn-retry/src/bn_retry - carryforward: true - business-digital-credentials: - paths: - - queue_services/business-digital-credentials/src/business_digital_credentials - carryforward: true - business-email-reminder: - paths: - - gcp-jobs/email-reminder/src/email_reminder + - colin-api/src/colin_api carryforward: true - business-emailer: + legalapi: paths: - - queue_services/business-emailer/src/business_emailer - carryforward: true - business-entity-bn: - paths: - - gcp-jobs/entity-bn/src/entity-bn - carryforward: true - business-expired-limited-restoration: - paths: - - gcp-jobs/expired-limited-restoration/src/expired_limited_restoration - carryforward: true - business-filer: - paths: - - queue_services/business-filer/src/business_filer - carryforward: true - business-filings-notebook-report: - paths: - - gcp-jobs/filings-notebook-report/src/notebookreport + - legal-api/src/legal_api carryforward: true - business-furnishings: + entityemailer: paths: - - gcp-jobs/furnishings/src/furnishings + - queue_services/entity-emailer/src/entity_emailer carryforward: true - business-future-effective-filings: + entityfiler: paths: - - gcp-jobs/future-effective-filings/src/future_effective_filings + - queue_services/entity-filer/src/entity_filer carryforward: true - business-involuntary-dissolutions: + entitypay: paths: - - gcp-jobs/involuntary-dissolutions/src/involuntary_dissolutions + - queue_services/entity-pay/src/entity_pay carryforward: true - business-pay: + entitybn: paths: - - queue_services/business-pay/src/business_pay + - queue_services/entity-bn/src/entity_bn carryforward: true - business-registry-model: + entity-digital-credentials: paths: - - python/common/business-registry-model/src/business_model + - queue_services/entity-digital-credentials/src/entity_digital_credentials carryforward: true - business-update-colin-filings: + furnishings: paths: - - gcp-jobs/update-colin-filings/src/update_colin_filings + - jobs/furnishings carryforward: true - business-update-legal-filings: + involuntary-dissolutions: paths: - - gcp-jobs/update-legal-filings/src/update_legal_filings + - jobs/involuntary-dissolutions carryforward: true From a504f025ac05e46a7464163eecb5a9ef3581b3ab Mon Sep 17 00:00:00 2001 From: panish16 Date: Wed, 3 Jun 2026 15:31:42 -0700 Subject: [PATCH 14/15] fix: use setup_pg8000_close_event_listener from cloud-sql-connector in legal-api --- legal-api/poetry.lock | 4 ++-- legal-api/src/legal_api/__init__.py | 27 ++------------------------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 826fc233e6..7617e0699d 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -556,7 +556,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloud-sql-connector" -version = "0.2.2" +version = "0.2.3" description = "Cloud SQL connection utilities for database connectivity with authentication and schema management" optional = false python-versions = "^3.9" @@ -573,7 +573,7 @@ sqlalchemy = ">=1.4.0,<3.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "cloud-sql-connector-python3.9" -resolved_reference = "e82cd710c818e55f7b468472b4908e4d14a25a79" +resolved_reference = "c4a2d6b465ecbb0d09695f91c44a1ee6fb0f970e" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index ab83f6dfb2..216bfa820e 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -35,14 +35,13 @@ This module is the API for the Legal Entity system. """ -import logging import os from typing import Optional +from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask, jsonify from registry_schemas import __version__ as registry_schemas_version from registry_schemas.flask import SchemaServices -from sqlalchemy import event from legal_api import config, models from legal_api.models import db @@ -63,28 +62,6 @@ setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first -def _setup_pg8000_graceful_shutdown(engine) -> None: - """Suppress pg8000 InterfaceError on connection close during Cloud Run scale-down.""" - try: - from pg8000.exceptions import InterfaceError as _interface_error # noqa: N813 - except ImportError: - _interface_error = None - - @event.listens_for(engine, "connect") - def on_connect(dbapi_conn, _connection_record): - orig_close = dbapi_conn.close - - def safe_close(): - try: - orig_close() - except Exception as exc: - if _interface_error and isinstance(exc, _interface_error): - logging.getLogger(__name__).debug("Suppressed pg8000 InterfaceError on teardown.") - else: - raise - - dbapi_conn.close = safe_close - def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: """Return a configured Flask App using the Factory method.""" @@ -108,7 +85,7 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: with app.app_context(): # db require app context digital_credentials.init_app(app) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - _setup_pg8000_graceful_shutdown(db.engine) + setup_pg8000_close_event_listener(db.engine) cache.init_app(app) From 1f360d8591ff3b292ae1e74f64b3b5c6e8109f2a Mon Sep 17 00:00:00 2001 From: panish16 Date: Wed, 3 Jun 2026 16:15:52 -0700 Subject: [PATCH 15/15] fix: revert legal-api to local pg8000 shutdown and fix poetry.lock ref Python 3.9 branch of sbc-connect-common does not have setup_pg8000_close_event_listener. Restore the local _setup_pg8000_graceful_shutdown function and point the lock back to the valid bcgov commit (e82cd710) so the Docker build can clone the dependency. --- legal-api/poetry.lock | 4 ++-- legal-api/src/legal_api/__init__.py | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 7617e0699d..826fc233e6 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -556,7 +556,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloud-sql-connector" -version = "0.2.3" +version = "0.2.2" description = "Cloud SQL connection utilities for database connectivity with authentication and schema management" optional = false python-versions = "^3.9" @@ -573,7 +573,7 @@ sqlalchemy = ">=1.4.0,<3.0.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "cloud-sql-connector-python3.9" -resolved_reference = "c4a2d6b465ecbb0d09695f91c44a1ee6fb0f970e" +resolved_reference = "e82cd710c818e55f7b468472b4908e4d14a25a79" subdirectory = "python/cloud-sql-connector" [[package]] diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index 216bfa820e..ab83f6dfb2 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -35,13 +35,14 @@ This module is the API for the Legal Entity system. """ +import logging import os from typing import Optional -from cloud_sql_connector import setup_pg8000_close_event_listener from flask import Flask, jsonify from registry_schemas import __version__ as registry_schemas_version from registry_schemas.flask import SchemaServices +from sqlalchemy import event from legal_api import config, models from legal_api.models import db @@ -62,6 +63,28 @@ setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first +def _setup_pg8000_graceful_shutdown(engine) -> None: + """Suppress pg8000 InterfaceError on connection close during Cloud Run scale-down.""" + try: + from pg8000.exceptions import InterfaceError as _interface_error # noqa: N813 + except ImportError: + _interface_error = None + + @event.listens_for(engine, "connect") + def on_connect(dbapi_conn, _connection_record): + orig_close = dbapi_conn.close + + def safe_close(): + try: + orig_close() + except Exception as exc: + if _interface_error and isinstance(exc, _interface_error): + logging.getLogger(__name__).debug("Suppressed pg8000 InterfaceError on teardown.") + else: + raise + + dbapi_conn.close = safe_close + def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: """Return a configured Flask App using the Factory method.""" @@ -85,7 +108,7 @@ def create_app(run_mode: Optional[str] = None, **kwargs) -> Flask: with app.app_context(): # db require app context digital_credentials.init_app(app) if app.config.get("CLOUDSQL_INSTANCE_CONNECTION_NAME"): # pragma: no cover - setup_pg8000_close_event_listener(db.engine) + _setup_pg8000_graceful_shutdown(db.engine) cache.init_app(app)