Skip to content

Commit 2a6e9fa

Browse files
committed
fix: Address code review comments on PSC DNS error handling
- Implement transient vs non-transient error detection in MonitoredCache. - Detect immutable instance DNS names using the corrected regex. - Avoid checking error message strings by using custom exception type (DnsLoopError). Address review comments: - kgala2: 'Should we have a try catch block of this sort to have a graceful shut in case of transient failures?' - hessjcg: 'Use an error code. Don'\''t check error message strings.' - hessjcg: 'The correct logic for detecting an immutable instance dns name: IF name matches /\.sql(-\w+)?\.goog$/ AND name does not match /\.global\.sql(-\w+)?\.goog$/'
1 parent bcc1cbf commit 2a6e9fa

4 files changed

Lines changed: 83 additions & 9 deletions

File tree

‎build.sh‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,17 +144,44 @@ function write_e2e_env(){
144144
done
145145
# Aliases for python e2e tests
146146
echo "export POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME=\"\$POSTGRES_CUSTOMER_CAS_DOMAIN_NAME\""
147+
echo "export MYSQL_USER_IAM='$(iam_user_mysql)'"
148+
echo "export POSTGRES_USER_IAM='$(iam_user_pg)'"
147149
echo "export POSTGRES_IAM_USER=\"\$POSTGRES_USER_IAM\""
148150
echo "export MYSQL_IAM_USER=\"\$MYSQL_USER_IAM\""
149151
} > "$1"
150152

151153
}
152154

153-
## with_venv - runs a command with the venv activated
154155
function with_venv() {
155156
"$@"
156157
}
157158

159+
function iam_user_pg() {
160+
local email
161+
local pguser
162+
163+
email="$(iam_user_email)"
164+
pguser="${email%%.gserviceaccount.com}"
165+
if [[ -n "$pguser" ]] ; then
166+
echo "$pguser"
167+
else
168+
echo "$email"
169+
fi
170+
}
171+
172+
function iam_user_mysql() {
173+
local email
174+
local mysqluser
175+
176+
email=$(iam_user_email)
177+
mysqluser="${email%%@*}"
178+
echo "$mysqluser"
179+
}
180+
181+
function iam_user_email() {
182+
gcloud auth list --format json | jq -r '.[] | select (.status == "ACTIVE") | .account'
183+
}
184+
158185
## help - prints the help details
159186
##
160187
function help() {

‎google/cloud/sql/connector/exceptions.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ class DnsResolutionError(Exception):
7373
"""
7474

7575

76+
class DnsLoopError(DnsResolutionError):
77+
"""
78+
Exception to be raised when a CNAME loop is detected during DNS resolution.
79+
"""
80+
81+
7682
class CacheClosedError(Exception):
7783
"""
7884
Exception to be raised when a ConnectionInfoCache can not be accessed after

‎google/cloud/sql/connector/monitored_cache.py‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@
1919
import ssl
2020
from typing import Any, Callable
2121

22+
import aiohttp
23+
2224
from google.cloud.sql.connector.connection_info import ConnectionInfo
2325
from google.cloud.sql.connector.connection_info import ConnectionInfoCache
2426
from google.cloud.sql.connector.exceptions import CacheClosedError
27+
from google.cloud.sql.connector.exceptions import DnsLoopError
2528
from google.cloud.sql.connector.instance import RefreshAheadCache
2629
from google.cloud.sql.connector.lazy import LazyRefreshCache
2730
from google.cloud.sql.connector.resolver import DefaultResolver
2831
from google.cloud.sql.connector.resolver import DnsResolver
32+
from google.cloud.sql.connector.resolver import is_instance_dns_name
2933

3034
logger = logging.getLogger(name=__name__)
3135

@@ -44,7 +48,16 @@ def __init__(
4448

4549
# If domain name is configured for instance and failover period is set,
4650
# poll for DNS record changes.
47-
if self.cache.conn_name.domain_name and failover_period > 0:
51+
is_instance_dns = False
52+
if self.cache.conn_name.domain_name:
53+
is_instance_dns = is_instance_dns_name(
54+
self.cache.conn_name.domain_name or ""
55+
)
56+
if (
57+
self.cache.conn_name.domain_name
58+
and failover_period > 0
59+
and not is_instance_dns
60+
):
4861
self.domain_name_ticker = asyncio.create_task(
4962
ticker(failover_period, self._check_domain_name)
5063
)
@@ -90,12 +103,28 @@ async def _check_domain_name(self) -> None:
90103
await self.close()
91104

92105
except Exception as e: # noqa: BLE001
93-
# Domain name checks should not be fatal, log error and continue.
94-
logger.debug(
95-
f"['{self.cache.conn_name}']: Unable to check domain name, "
96-
f"domain name {self.cache.conn_name.domain_name} did not "
97-
f"resolve: {e}"
106+
if self._is_non_transient(e):
107+
logger.debug(
108+
f"['{self.cache.conn_name}']: Domain name check failed with "
109+
f"non-transient error, closing all connections: {e}"
110+
)
111+
await self.close()
112+
else:
113+
# Domain name checks should not be fatal for transient errors, log error and continue.
114+
logger.debug(
115+
f"['{self.cache.conn_name}']: Unable to check domain name, "
116+
f"domain name {self.cache.conn_name.domain_name} did not "
117+
f"resolve (transient error): {e}"
118+
)
119+
120+
def _is_non_transient(self, e: Exception) -> bool:
121+
return (
122+
isinstance(e, (ValueError, DnsLoopError))
123+
or (
124+
isinstance(e, aiohttp.ClientResponseError)
125+
and e.status in (403, 404)
98126
)
127+
)
99128

100129
async def connect_info(self) -> ConnectionInfo:
101130
if self.closed:

‎google/cloud/sql/connector/resolver.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,24 @@
2525
_parse_connection_name_with_domain_name,
2626
)
2727
from google.cloud.sql.connector.connection_name import ConnectionName
28+
from google.cloud.sql.connector.exceptions import DnsLoopError
2829
from google.cloud.sql.connector.exceptions import DnsResolutionError
2930

3031
PSC_DNS_PATTERN = re.compile(
3132
r"^([a-f0-9]{12})\.([^.]+)\.([a-z0-9]+-[a-z0-9]+)\.(sql|sql-psa|sql-psc)\.goog\.?$"
3233
)
3334

35+
INSTANCE_DNS_PATTERN = re.compile(r"\.sql(-\w+)?\.goog\.?$")
36+
GLOBAL_INSTANCE_DNS_PATTERN = re.compile(r"\.global\.sql(-\w+)?\.goog\.?$")
37+
38+
39+
def is_instance_dns_name(name: str) -> bool:
40+
lower = name.lower()
41+
return bool(
42+
INSTANCE_DNS_PATTERN.search(lower)
43+
and not GLOBAL_INSTANCE_DNS_PATTERN.search(lower)
44+
)
45+
3446

3547
class DefaultResolver:
3648
"""DefaultResolver simply validates and parses instance connection name."""
@@ -102,7 +114,7 @@ async def resolve(self, dns: str) -> ConnectionName: # type: ignore
102114

103115
if cname_found:
104116
if cname in visited:
105-
raise DnsResolutionError(f"CNAME loop detected for `{dns}`")
117+
raise DnsLoopError(f"CNAME loop detected for `{dns}`")
106118
visited.add(cname)
107119
current = cname
108120
continue
@@ -130,7 +142,7 @@ async def resolve(self, dns: str) -> ConnectionName: # type: ignore
130142
else f"Unable to resolve TXT record for `{current}`"
131143
)
132144

133-
raise DnsResolutionError(
145+
raise DnsLoopError(
134146
f"CNAME loop detected or max resolution depth reached for `{dns}`"
135147
)
136148

0 commit comments

Comments
 (0)