Skip to content

Commit a0aaafc

Browse files
feat: No automatic close (#71)
* don't automatically close connection in finalizer * fix unit tests unclosed connections * fix integration tests unclosed connections * add unclosed connection warning test * fix integration tests
1 parent cadd629 commit a0aaafc

File tree

7 files changed

+78
-54
lines changed

7 files changed

+78
-54
lines changed

src/firebolt/async_db/_types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ class _InternalType(Enum):
9494
"""Enum of all internal firebolt types except for array."""
9595

9696
# INT, INTEGER
97+
Int8 = "Int8"
9798
UInt8 = "UInt8"
99+
Int16 = "Int16"
98100
UInt16 = "UInt16"
99101
Int32 = "Int32"
100102
UInt32 = "UInt32"
@@ -125,7 +127,9 @@ class _InternalType(Enum):
125127
def python_type(self) -> type:
126128
"""Convert internal type to python type."""
127129
types = {
130+
_InternalType.Int8: int,
128131
_InternalType.UInt8: int,
132+
_InternalType.Int16: int,
129133
_InternalType.UInt16: int,
130134
_InternalType.Int32: int,
131135
_InternalType.UInt32: int,

src/firebolt/db/connection.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from __future__ import annotations
22

3-
from asyncio import new_event_loop
43
from functools import wraps
54
from inspect import cleandoc
65
from types import TracebackType
76
from typing import Any
7+
from warnings import warn
88

99
from readerwriterlock.rwlock import RWLockWrite
1010

@@ -38,7 +38,7 @@ class Connection(AsyncBaseConnection):
3838
"""
3939
)
4040

41-
__slots__ = AsyncBaseConnection.__slots__ + ("_closing_lock", "_loop")
41+
__slots__ = AsyncBaseConnection.__slots__ + ("_closing_lock",)
4242

4343
cursor_class = Cursor
4444

@@ -47,7 +47,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
4747
# Holding this lock for write means that connection is closing itself.
4848
# cursor() should hold this lock for read to read/write state
4949
self._closing_lock = RWLockWrite()
50-
self._loop = new_event_loop()
5150

5251
@wraps(AsyncBaseConnection.cursor)
5352
def cursor(self) -> Cursor:
@@ -59,9 +58,7 @@ def cursor(self) -> Cursor:
5958
@wraps(AsyncBaseConnection._aclose)
6059
def close(self) -> None:
6160
with self._closing_lock.gen_wlock():
62-
if not self.closed:
63-
self._loop.run_until_complete(self._aclose())
64-
self._loop.close()
61+
async_to_sync(self._aclose)()
6562

6663
# Context manager support
6764
def __enter__(self) -> Connection:
@@ -75,7 +72,8 @@ def __exit__(
7572
self.close()
7673

7774
def __del__(self) -> None:
78-
self.close()
75+
if not self.closed:
76+
warn(f"Unclosed {self!r}", UserWarning)
7977

8078

8179
connect = async_to_sync(async_connect_factory(Connection))

tests/integration/dbapi/conftest.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,22 @@ def api_endpoint() -> str:
7070
@fixture
7171
def all_types_query() -> str:
7272
return (
73-
"select 1 as uint8, 258 as uint16, 80000 as uint32, -30000 as int32,"
74-
"30000000000 as uint64, -30000000000 as int64, cast(1.23 AS FLOAT) as float32,"
75-
" 1.2345678901234 as float64, 'text' as \"string\", "
76-
"CAST('2021-03-28' AS DATE) as \"date\", "
77-
'CAST(\'2019-07-31 01:01:01\' AS DATETIME) as "datetime", true as "bool",'
78-
'[1,2,3,4] as "array", cast(null as int) as nullable'
73+
"select 1 as uint8, -1 as int8, 257 as uint16, -257 as int16, 80000 as uint32,"
74+
" -80000 as int32, 30000000000 as uint64, -30000000000 as int64, cast(1.23 AS"
75+
" FLOAT) as float32, 1.2345678901234 as float64, 'text' as \"string\","
76+
" CAST('2021-03-28' AS DATE) as \"date\", CAST('2019-07-31 01:01:01' AS"
77+
' DATETIME) as "datetime", true as "bool",[1,2,3,4] as "array", cast(null as'
78+
" int) as nullable"
7979
)
8080

8181

8282
@fixture
8383
def all_types_query_description() -> List[Column]:
8484
return [
8585
Column("uint8", int, None, None, None, None, None),
86+
Column("int8", int, None, None, None, None, None),
8687
Column("uint16", int, None, None, None, None, None),
88+
Column("int16", int, None, None, None, None, None),
8789
Column("uint32", int, None, None, None, None, None),
8890
Column("int32", int, None, None, None, None, None),
8991
Column("uint64", int, None, None, None, None, None),
@@ -104,9 +106,11 @@ def all_types_query_response() -> List[ColType]:
104106
return [
105107
[
106108
1,
107-
258,
109+
-1,
110+
257,
111+
-257,
108112
80000,
109-
-30000,
113+
-80000,
110114
30000000000,
111115
-30000000000,
112116
1.23,

tests/integration/dbapi/sync/conftest.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
def connection(
88
engine_url: str, database_name: str, username: str, password: str, api_endpoint: str
99
) -> Connection:
10-
return connect(
10+
connection = connect(
1111
engine_url=engine_url,
1212
database=database_name,
1313
username=username,
1414
password=password,
1515
api_endpoint=api_endpoint,
1616
)
17+
yield connection
18+
connection.close()
1719

1820

1921
@fixture
@@ -24,10 +26,12 @@ def connection_engine_name(
2426
password: str,
2527
api_endpoint: str,
2628
) -> Connection:
27-
return connect(
29+
connection = connect(
2830
engine_name=engine_name,
2931
database=database_name,
3032
username=username,
3133
password=password,
3234
api_endpoint=api_endpoint,
3335
)
36+
yield connection
37+
connection.close()

tests/integration/dbapi/sync/test_errors.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,34 @@ def test_invalid_credentials(
1515
engine_url: str, database_name: str, username: str, password: str, api_endpoint: str
1616
) -> None:
1717
"""Connection properly reacts to invalid credentials error"""
18-
connection = connect(
18+
with connect(
1919
engine_url=engine_url,
2020
database=database_name,
2121
username=username + "_",
2222
password=password + "_",
2323
api_endpoint=api_endpoint,
24-
)
25-
with raises(AuthenticationError) as exc_info:
26-
connection.cursor().execute("show tables")
24+
) as connection:
25+
with raises(AuthenticationError) as exc_info:
26+
connection.cursor().execute("show tables")
2727

28-
assert str(exc_info.value).startswith(
29-
"Failed to authenticate"
30-
), "Invalid authentication error message"
28+
assert str(exc_info.value).startswith(
29+
"Failed to authenticate"
30+
), "Invalid authentication error message"
3131

3232

3333
def test_engine_url_not_exists(
3434
engine_url: str, database_name: str, username: str, password: str, api_endpoint: str
3535
) -> None:
3636
"""Connection properly reacts to invalid engine url error"""
37-
connection = connect(
37+
with connect(
3838
engine_url=engine_url + "_",
3939
database=database_name,
4040
username=username,
4141
password=password,
4242
api_endpoint=api_endpoint,
43-
)
44-
with raises(ConnectError):
45-
connection.cursor().execute("show tables")
43+
) as connection:
44+
with raises(ConnectError):
45+
connection.cursor().execute("show tables")
4646

4747

4848
def test_engine_name_not_exists(
@@ -54,14 +54,14 @@ def test_engine_name_not_exists(
5454
) -> None:
5555
"""Connection properly reacts to invalid engine name error"""
5656
with raises(FireboltEngineError):
57-
connection = connect(
57+
with connect(
5858
engine_name=engine_name + "_________",
5959
database=database_name,
6060
username=username,
6161
password=password,
6262
api_endpoint=api_endpoint,
63-
)
64-
connection.cursor().execute("show tables")
63+
) as connection:
64+
connection.cursor().execute("show tables")
6565

6666

6767
def test_engine_stopped(
@@ -73,34 +73,34 @@ def test_engine_stopped(
7373
) -> None:
7474
"""Connection properly reacts to engine not running error"""
7575
with raises(EngineNotRunningError):
76-
connection = connect(
76+
with connect(
7777
engine_url=stopped_engine_url,
7878
database=database_name,
7979
username=username,
8080
password=password,
8181
api_endpoint=api_endpoint,
82-
)
83-
connection.cursor().execute("show tables")
82+
) as connection:
83+
connection.cursor().execute("show tables")
8484

8585

8686
def test_database_not_exists(
8787
engine_url: str, database_name: str, username: str, password: str, api_endpoint: str
8888
) -> None:
8989
"""Connection properly reacts to invalid database error"""
9090
new_db_name = database_name + "_"
91-
connection = connect(
91+
with connect(
9292
engine_url=engine_url,
9393
database=new_db_name,
9494
username=username,
9595
password=password,
9696
api_endpoint=api_endpoint,
97-
)
98-
with raises(FireboltDatabaseError) as exc_info:
99-
connection.cursor().execute("show tables")
97+
) as connection:
98+
with raises(FireboltDatabaseError) as exc_info:
99+
connection.cursor().execute("show tables")
100100

101-
assert (
102-
str(exc_info.value) == f"Database {new_db_name} does not exist"
103-
), "Invalid database name error message"
101+
assert (
102+
str(exc_info.value) == f"Database {new_db_name} does not exist"
103+
), "Invalid database name error message"
104104

105105

106106
def test_sql_error(connection: Connection) -> None:

tests/unit/db/test_connection.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Callable, List
22

33
from httpx import codes
4-
from pytest import raises
4+
from pytest import raises, warns
55
from pytest_httpx import HTTPXMock
66

77
from firebolt.async_db._types import ColType
@@ -54,24 +54,28 @@ def test_cursor_initialized(
5454
httpx_mock.add_callback(query_callback, url=query_url)
5555

5656
for url in (settings.server, f"https://{settings.server}"):
57-
connection = connect(
57+
with connect(
5858
engine_url=url,
5959
database=db_name,
6060
username="u",
6161
password="p",
6262
api_endpoint=settings.server,
63-
)
63+
) as connection:
6464

65-
cursor = connection.cursor()
66-
assert cursor.connection == connection, "Invalid cursor connection attribute"
67-
assert cursor._client == connection._client, "Invalid cursor _client attribute"
65+
cursor = connection.cursor()
66+
assert (
67+
cursor.connection == connection
68+
), "Invalid cursor connection attribute"
69+
assert (
70+
cursor._client == connection._client
71+
), "Invalid cursor _client attribute"
6872

69-
assert cursor.execute("select*") == len(python_query_data)
73+
assert cursor.execute("select*") == len(python_query_data)
7074

71-
cursor.close()
72-
assert (
73-
cursor not in connection._cursors
74-
), "Cursor wasn't removed from connection after close"
75+
cursor.close()
76+
assert (
77+
cursor not in connection._cursors
78+
), "Cursor wasn't removed from connection after close"
7579

7680

7781
def test_connect_empty_parameters():
@@ -154,3 +158,13 @@ def test_connect_engine_name(
154158
api_endpoint=settings.server,
155159
) as connection:
156160
assert connection.cursor().execute("select*") == len(python_query_data)
161+
162+
163+
def test_connection_unclosed_warnings():
164+
c = Connection("", "", "", "", "")
165+
with warns(UserWarning) as winfo:
166+
del c
167+
168+
assert "Unclosed" in str(
169+
winfo.list[0].message
170+
), "Invalid unclosed connection warning"

tests/unit/service/test_engine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ def test_get_connection(
166166
manager = ResourceManager(settings=settings)
167167
engine = manager.engines.create(name=engine_name)
168168

169-
connection = engine.get_connection()
170-
assert connection
169+
with engine.get_connection() as connection:
170+
assert connection
171171

172172

173173
def test_attach_to_database(

0 commit comments

Comments
 (0)