1818
1919import asyncio
2020from functools import partial
21+ import ipaddress
2122import logging
2223import os
2324import socket
4950
5051logger = logging .getLogger (name = __name__ )
5152
53+
54+ def _is_ip_address (ip : str ) -> bool :
55+ try :
56+ ipaddress .ip_address (ip )
57+ return True
58+ except ValueError :
59+ return False
60+
61+
62+ def _get_fallback_ips (
63+ current_ips : list [str ], ip_addresses : dict [str , list [str ]]
64+ ) -> list [str ]:
65+ if not current_ips :
66+ return current_ips
67+ if _is_ip_address (current_ips [0 ]):
68+ return current_ips
69+ fallback = ip_addresses .get ("PRIVATE" )
70+ if not fallback :
71+ fallback = ip_addresses .get ("PRIMARY" )
72+ return fallback if fallback else current_ips
73+
5274ASYNC_DRIVERS = ["asyncpg" ]
5375SERVER_PROXY_PORT = 3307
5476_DEFAULT_SCHEME = "https://"
@@ -316,6 +338,8 @@ async def connect_async(
316338 user_agent = self ._user_agent ,
317339 driver = driver ,
318340 )
341+ if hasattr (self ._resolver , "set_client" ):
342+ self ._resolver .set_client (self ._client )
319343 enable_iam_auth = kwargs .pop ("enable_iam_auth" , self ._enable_iam_auth )
320344
321345 conn_name = await self ._resolver .resolve (instance_connection_string )
@@ -384,40 +408,50 @@ async def connect_async(
384408 conn_info = await monitored_cache .connect_info ()
385409 # validate driver matches intended database engine
386410 DriverMapping .validate_engine (driver , conn_info .database_version )
387- ip_address = conn_info .get_preferred_ip (ip_type )
411+ preferred_ips = conn_info .get_preferred_ips (ip_type )
388412 except Exception :
389413 # with an error from Cloud SQL Admin API call or IP type, invalidate
390414 # the cache and re-raise the error
391415 await self ._remove_cached (str (conn_name ), enable_iam_auth )
392416 raise
393417
418+ targets = []
394419 # If the connector is configured with a custom DNS name, attempt to use
395420 # that DNS name to connect to the instance. Fall back to the metadata IP
396421 # address if the DNS name does not resolve to an IP address.
397422 if conn_info .conn_name .domain_name and isinstance (self ._resolver , DnsResolver ):
398423 try :
399424 ips = await self ._resolver .resolve_a_record (conn_info .conn_name .domain_name )
400425 if ips :
401- ip_address = ips [ 0 ]
426+ targets . extend ( ips )
402427 logger .debug (
403428 f"['{ instance_connection_string } ']: Custom DNS name "
404- f"'{ conn_info .conn_name .domain_name } ' resolved to '{ ip_address } ', "
429+ f"'{ conn_info .conn_name .domain_name } ' resolved to '{ ips } ', "
405430 "using it to connect"
406431 )
407432 else :
433+ fallback_ips = _get_fallback_ips (
434+ preferred_ips , conn_info .ip_addrs
435+ )
408436 logger .debug (
409437 f"['{ instance_connection_string } ']: Custom DNS name "
410438 f"'{ conn_info .conn_name .domain_name } ' resolved but returned no "
411- f"entries, using '{ ip_address } ' from instance metadata"
439+ f"entries, using '{ fallback_ips [ 0 ] } ' from instance metadata"
412440 )
441+ targets .extend (fallback_ips )
413442 except Exception as e :
443+ fallback_ips = _get_fallback_ips (
444+ preferred_ips , conn_info .ip_addrs
445+ )
414446 logger .debug (
415447 f"['{ instance_connection_string } ']: Custom DNS name "
416448 f"'{ conn_info .conn_name .domain_name } ' did not resolve to an IP "
417- f"address: { e } , using '{ ip_address } ' from instance metadata"
449+ f"address: { e } , using '{ fallback_ips [ 0 ] } ' from instance metadata"
418450 )
451+ targets .extend (fallback_ips )
452+ else :
453+ targets .extend (preferred_ips )
419454
420- logger .debug (f"['{ conn_info .conn_name } ']: Connecting to { ip_address } :3307" )
421455 # format `user` param for automatic IAM database authn
422456 if enable_iam_auth :
423457 formatted_user = format_database_user (
@@ -428,32 +462,56 @@ async def connect_async(
428462 f"['{ instance_connection_string } ']: Truncated IAM database username from { kwargs ['user' ]} to { formatted_user } "
429463 )
430464 kwargs ["user" ] = formatted_user
465+
431466 try :
432- # async drivers are unblocking and can be awaited directly
433- if driver in ASYNC_DRIVERS :
434- return await connector (
435- ip_address ,
436- await conn_info .create_ssl_context (enable_iam_auth ),
437- ** kwargs ,
438- )
439- # Create socket with SSLContext for sync drivers
440- ctx = await conn_info .create_ssl_context (enable_iam_auth )
441- sock = ctx .wrap_socket (
442- socket .create_connection ((ip_address , SERVER_PROXY_PORT )),
443- server_hostname = ip_address ,
444- )
445- # If this connection was opened using a domain name, then store it
446- # for later in case we need to forcibly close it on failover.
447- if conn_info .conn_name .domain_name :
448- monitored_cache .sockets .append (sock )
449- # Synchronous drivers are blocking and run using executor
450- connect_partial = partial (
451- connector ,
452- ip_address ,
453- sock ,
454- ** kwargs ,
455- )
456- return await self ._loop .run_in_executor (None , connect_partial )
467+ last_ex = None
468+ for target_ip in targets :
469+ logger .debug (f"['{ conn_info .conn_name } ']: Connecting to { target_ip } :3307" )
470+ try :
471+ # async drivers are unblocking and can be awaited directly
472+ if driver in ASYNC_DRIVERS :
473+ conn = await connector (
474+ target_ip ,
475+ await conn_info .create_ssl_context (enable_iam_auth ),
476+ ** kwargs ,
477+ )
478+ last_ex = None
479+ return conn
480+
481+ # Create socket with SSLContext for sync drivers
482+ ctx = await conn_info .create_ssl_context (enable_iam_auth )
483+ raw_sock = socket .create_connection ((target_ip , SERVER_PROXY_PORT ))
484+ try :
485+ sock = ctx .wrap_socket (
486+ raw_sock ,
487+ server_hostname = target_ip ,
488+ )
489+ except Exception :
490+ raw_sock .close ()
491+ raise
492+
493+ # If this connection was opened using a domain name, then store it
494+ # for later in case we need to forcibly close it on failover.
495+ if conn_info .conn_name .domain_name :
496+ monitored_cache .sockets .append (sock )
497+ # Synchronous drivers are blocking and run using executor
498+ connect_partial = partial (
499+ connector ,
500+ target_ip ,
501+ sock ,
502+ ** kwargs ,
503+ )
504+ conn = await self ._loop .run_in_executor (None , connect_partial )
505+ last_ex = None
506+ return conn
507+ except Exception as e :
508+ logger .debug (
509+ f"['{ conn_info .conn_name } ']: Connection to { target_ip } failed: { e } "
510+ )
511+ last_ex = e
512+
513+ if last_ex :
514+ raise last_ex
457515
458516 except Exception :
459517 # with any exception, we attempt a force refresh, then throw the error
0 commit comments