Skip to content

Commit 372e401

Browse files
fix: resolve TimeoutError and add context manager to Connector (#309)
1 parent a5da760 commit 372e401

File tree

4 files changed

+134
-22
lines changed

4 files changed

+134
-22
lines changed

README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,92 @@ with pool.connect() as db_conn:
100100

101101
**Note for SQL Server users**: If your SQL Server instance requires SSL, you need to download the CA certificate for your instance and include `cafile={path to downloaded certificate}` and `validate_host=False`. This is a workaround for a [known issue](https://issuetracker.google.com/184867147).
102102

103+
### Custom Connector Object
104+
105+
If you need to customize something about the connector, or want to specify
106+
defaults for each connection to make, you can initialize a custom
107+
`Connector` object directly:
108+
109+
```python
110+
from google.cloud.sql.connector import Connector
111+
112+
# Note: all parameters below are optional
113+
connector = Connector(
114+
ip_type=IPTypes.PUBLIC,
115+
enable_iam_auth=False,
116+
timeout=30,
117+
credentials=custom_creds # google.auth.credentials.Credentials
118+
)
119+
```
120+
121+
You can then call the Connector object's `connect` method as you
122+
would the default `connector.connect`:
123+
124+
```python
125+
def getconn() -> pymysql.connections.Connection:
126+
conn = connector.connect(
127+
"project:region:instance",
128+
"pymysql",
129+
user="root",
130+
password="shhh",
131+
db="your-db-name"
132+
)
133+
return conn
134+
```
135+
136+
To close the `Connector` object's background resources, call it's `close()` method as follows:
137+
138+
```python
139+
connector.close()
140+
```
141+
142+
### Using Connector as a Context Manager
143+
144+
The `Connector` object can also be used as a context manager in order to
145+
automatically close and cleanup resources, removing the need for explicit
146+
calls to `connector.close()`.
147+
148+
Connector as a context manager:
149+
150+
```python
151+
from google.cloud.sql.connector import Connector
152+
153+
# build connection
154+
def getconn() -> pymysql.connections.Connection:
155+
with Connector() as connector:
156+
conn = connector.connect(
157+
"project:region:instance",
158+
"pymysql",
159+
user="root",
160+
password="shhh",
161+
db="your-db-name"
162+
)
163+
return conn
164+
165+
# create connection pool
166+
pool = sqlalchemy.create_engine(
167+
"mysql+pymysql://",
168+
creator=getconn,
169+
)
170+
171+
# insert statement
172+
insert_stmt = sqlalchemy.text(
173+
"INSERT INTO my_table (id, title) VALUES (:id, :title)",
174+
)
175+
176+
# interact with Cloud SQL database using connection pool
177+
with pool.connect() as db_conn:
178+
# insert into database
179+
db_conn.execute(insert_stmt, id="book1", title="Book One")
180+
181+
# query database
182+
result = db_conn.execute("SELECT * from my_table").fetchall()
183+
184+
# Do something with the results
185+
for row in result:
186+
print(row)
187+
```
188+
103189
### Specifying Public or Private IP
104190
The Cloud SQL Connector for Python can be used to connect to Cloud SQL instances using both public and private IP addresses. To specify which IP address to use to connect, set the `ip_type` keyword argument Possible values are `IPTypes.PUBLIC` and `IPTypes.PRIVATE`.
105191
Example:

google/cloud/sql/connector/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
from typing import List
1818

19-
from .connector import connect
19+
from .connector import connect, Connector
2020
from .instance_connection_manager import IPTypes
2121

2222

23-
__ALL__ = [connect, IPTypes]
23+
__ALL__ = [connect, Connector, IPTypes]
2424

2525
try:
2626
import pkg_resources

google/cloud/sql/connector/connector.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@
1616
import asyncio
1717
import concurrent
1818
import logging
19+
from types import TracebackType
1920
from google.cloud.sql.connector.instance_connection_manager import (
2021
InstanceConnectionManager,
2122
IPTypes,
2223
)
2324
from google.cloud.sql.connector.utils import generate_keys
2425
from google.auth.credentials import Credentials
2526
from threading import Thread
26-
from typing import Any, Dict, Optional
27+
from typing import Any, Dict, Optional, Type
2728

2829
logger = logging.getLogger(name=__name__)
2930

@@ -182,22 +183,29 @@ async def connect_async(
182183
icm.force_refresh()
183184
raise (e)
184185

185-
async def _close(self) -> None:
186-
"""Helper function to close InstanceConnectionManagers' tasks."""
187-
await asyncio.gather(*[icm.close() for icm in self._instances.values()])
186+
def __enter__(self) -> Any:
187+
"""Enter context manager by returning Connector object"""
188+
return self
188189

189-
def __del__(self) -> None:
190-
"""Deconstructor to make sure InstanceConnectionManagers are closed
191-
and tasks have finished to have a graceful exit.
192-
"""
193-
logger.debug("Entering deconstructor")
190+
def __exit__(
191+
self,
192+
exc_type: Optional[Type[BaseException]],
193+
exc_val: Optional[BaseException],
194+
exc_tb: Optional[TracebackType],
195+
) -> None:
196+
"""Exit context manager by closing Connector"""
197+
self.close()
194198

195-
deconstruct_future = asyncio.run_coroutine_threadsafe(
196-
self._close(), loop=self._loop
197-
)
199+
def close(self) -> None:
200+
"""Close Connector by stopping tasks and releasing resources."""
201+
close_future = asyncio.run_coroutine_threadsafe(self._close(), loop=self._loop)
198202
# Will attempt to safely shut down tasks for 5s
199-
deconstruct_future.result(timeout=5)
200-
logger.debug("Finished deconstructing")
203+
close_future.result(timeout=5)
204+
205+
async def _close(self) -> None:
206+
"""Helper function to cancel InstanceConnectionManagers' tasks
207+
and close aiohttp.ClientSession."""
208+
await asyncio.gather(*[icm.close() for icm in self._instances.values()])
201209

202210

203211
def connect(instance_connection_string: str, driver: str, **kwargs: Any) -> Any:

tests/system/test_connector_object.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
import sqlalchemy
1919
import logging
2020
import google.auth
21-
from google.cloud.sql.connector import connector
21+
from google.cloud.sql.connector import Connector
2222
import datetime
2323
import concurrent.futures
2424

2525

2626
def init_connection_engine(
27-
custom_connector: connector.Connector,
27+
custom_connector: Connector,
2828
) -> sqlalchemy.engine.Engine:
2929
def getconn() -> pymysql.connections.Connection:
3030
conn = custom_connector.connect(
@@ -48,7 +48,7 @@ def test_connector_with_credentials() -> None:
4848
credentials, project = google.auth.load_credentials_from_file(
4949
os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
5050
)
51-
custom_connector = connector.Connector(credentials=credentials)
51+
custom_connector = Connector(credentials=credentials)
5252
try:
5353
pool = init_connection_engine(custom_connector)
5454

@@ -57,12 +57,14 @@ def test_connector_with_credentials() -> None:
5757

5858
except Exception as e:
5959
logging.exception("Failed to connect with credentials from file!", e)
60+
# close connector
61+
custom_connector.close()
6062

6163

6264
def test_multiple_connectors() -> None:
6365
"""Test that same Cloud SQL instance can connect with two Connector objects."""
64-
first_connector = connector.Connector()
65-
second_connector = connector.Connector()
66+
first_connector = Connector()
67+
second_connector = Connector()
6668
try:
6769
pool = init_connection_engine(first_connector)
6870
pool2 = init_connection_engine(second_connector)
@@ -83,6 +85,10 @@ def test_multiple_connectors() -> None:
8385
except Exception as e:
8486
logging.exception("Failed to connect with multiple Connector objects!", e)
8587

88+
# close connectors
89+
first_connector.close()
90+
second_connector.close()
91+
8692

8793
def test_connector_in_ThreadPoolExecutor() -> None:
8894
"""Test that Connector can connect from ThreadPoolExecutor thread.
@@ -91,16 +97,28 @@ def test_connector_in_ThreadPoolExecutor() -> None:
9197

9298
def get_time() -> datetime.datetime:
9399
"""Helper method for getting current time from database."""
94-
default_connector = connector.Connector()
100+
default_connector = Connector()
95101
pool = init_connection_engine(default_connector)
96102

97103
# connect to database and get current time
98104
with pool.connect() as conn:
99105
current_time = conn.execute("SELECT NOW()").fetchone()
106+
107+
# close connector
108+
default_connector.close()
100109
return current_time[0]
101110

102111
# try running connector in ThreadPoolExecutor as Cloud Run does
103112
with concurrent.futures.ThreadPoolExecutor() as executor:
104113
future = executor.submit(get_time)
105114
return_value = future.result()
106115
assert isinstance(return_value, datetime.datetime)
116+
117+
118+
def test_connector_as_context_manager() -> None:
119+
"""Test that Connector can be used as a context manager."""
120+
with Connector() as connector:
121+
pool = init_connection_engine(connector)
122+
123+
with pool.connect() as conn:
124+
conn.execute("SELECT 1")

0 commit comments

Comments
 (0)