Skip to content

Commit 342a9f5

Browse files
authored
Deprecate implicit closing of drivers and sessions (#653)
Deprecated closing of driver and session objects in their destructor. This behaviour is non-deterministic as there is no guarantee that the destructor will ever be called. A `ResourceWarning` is emitted instead.
1 parent 47926e4 commit 342a9f5

File tree

8 files changed

+110
-32
lines changed

8 files changed

+110
-32
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060
Use `Session.last_bookmarks` instead.
6161
- `neo4j.Bookmark` was deprecated.
6262
Use `neo4j.Bookmarks` instead.
63+
- Deprecated closing of driver and session objects in their destructor.
64+
This behaviour is non-deterministic as there is no guarantee that the
65+
destructor will ever be called. A `ResourceWarning` is emitted instead.
66+
Make sure to configure Python to output those warnings when developing your
67+
application locally (it does not by default).
6368

6469

6570
## Version 4.4

neo4j/_async/driver.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
# limitations under the License.
1717

1818

19+
import asyncio
20+
1921
from .._async_compat.util import AsyncUtil
2022
from ..addressing import Address
2123
from ..api import READ_ACCESS
@@ -25,7 +27,11 @@
2527
SessionConfig,
2628
WorkspaceConfig,
2729
)
28-
from ..meta import experimental
30+
from ..meta import (
31+
deprecation_warn,
32+
experimental,
33+
unclosed_resource_warn,
34+
)
2935

3036

3137
class AsyncGraphDatabase:
@@ -189,6 +195,9 @@ class AsyncDriver:
189195
#: Connection pool
190196
_pool = None
191197

198+
#: Flag if the driver has been closed
199+
_closed = False
200+
192201
def __init__(self, pool):
193202
assert pool is not None
194203
self._pool = pool
@@ -200,8 +209,19 @@ async def __aexit__(self, exc_type, exc_value, traceback):
200209
await self.close()
201210

202211
def __del__(self):
203-
if not AsyncUtil.is_async_code:
204-
self.close()
212+
if not self._closed:
213+
unclosed_resource_warn(self)
214+
# TODO: 6.0 - remove this
215+
if not self._closed:
216+
if not AsyncUtil.is_async_code:
217+
deprecation_warn(
218+
"Relying on AsyncDriver's destructor to close the session "
219+
"is deprecated. Please make sure to close the session. "
220+
"Use it as a context (`with` statement) or make sure to "
221+
"call `.close()` explicitly. Future versions of the "
222+
"driver will not close drivers automatically."
223+
)
224+
self.close()
205225

206226
@property
207227
def encrypted(self):
@@ -225,6 +245,7 @@ async def close(self):
225245
""" Shut down, closing any open connections in the pool.
226246
"""
227247
await self._pool.close()
248+
self._closed = True
228249

229250
@experimental("The configuration may change in the future.")
230251
async def verify_connectivity(self, **config):

neo4j/_async/work/session.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,22 +84,11 @@ class AsyncSession(AsyncWorkspace):
8484
# The state this session is in.
8585
_state_failed = False
8686

87-
# Session have been properly closed.
88-
_closed = False
89-
9087
def __init__(self, pool, session_config):
9188
super().__init__(pool, session_config)
9289
assert isinstance(session_config, SessionConfig)
9390
self._bookmarks = self._prepare_bookmarks(session_config.bookmarks)
9491

95-
def __del__(self):
96-
if asyncio.iscoroutinefunction(self.close):
97-
return
98-
try:
99-
self.close()
100-
except (OSError, ServiceUnavailable, SessionExpired):
101-
pass
102-
10392
async def __aenter__(self):
10493
return self
10594

neo4j/_async/work/workspace.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@
1919
import asyncio
2020

2121
from ...conf import WorkspaceConfig
22-
from ...exceptions import ServiceUnavailable
22+
from ...exceptions import (
23+
ServiceUnavailable,
24+
SessionExpired,
25+
)
26+
from ...meta import (
27+
deprecation_warn,
28+
unclosed_resource_warn,
29+
)
2330
from ..io import AsyncNeo4jPool
2431

2532

@@ -34,13 +41,25 @@ def __init__(self, pool, config):
3441
# Sessions are supposed to cache the database on which to operate.
3542
self._cached_database = False
3643
self._bookmarks = None
44+
# Workspace has been closed.
45+
self._closed = False
3746

3847
def __del__(self):
48+
if not self._closed:
49+
unclosed_resource_warn(self)
50+
# TODO: 6.0 - remove this
3951
if asyncio.iscoroutinefunction(self.close):
4052
return
4153
try:
54+
deprecation_warn(
55+
"Relying on AsyncSession's destructor to close the session "
56+
"is deprecated. Please make sure to close the session. Use it "
57+
"as a context (`with` statement) or make sure to call "
58+
"`.close()` explicitly. Future versions of the driver will "
59+
"not close sessions automatically."
60+
)
4261
self.close()
43-
except OSError:
62+
except (OSError, ServiceUnavailable, SessionExpired):
4463
pass
4564

4665
async def __aenter__(self):
@@ -100,3 +119,4 @@ async def _disconnect(self, sync=False):
100119

101120
async def close(self):
102121
await self._disconnect(sync=True)
122+
self._closed = True

neo4j/_sync/driver.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
# limitations under the License.
1717

1818

19+
import asyncio
20+
1921
from .._async_compat.util import Util
2022
from ..addressing import Address
2123
from ..api import READ_ACCESS
@@ -25,7 +27,11 @@
2527
SessionConfig,
2628
WorkspaceConfig,
2729
)
28-
from ..meta import experimental
30+
from ..meta import (
31+
deprecation_warn,
32+
experimental,
33+
unclosed_resource_warn,
34+
)
2935

3036

3137
class GraphDatabase:
@@ -189,6 +195,9 @@ class Driver:
189195
#: Connection pool
190196
_pool = None
191197

198+
#: Flag if the driver has been closed
199+
_closed = False
200+
192201
def __init__(self, pool):
193202
assert pool is not None
194203
self._pool = pool
@@ -200,8 +209,19 @@ def __exit__(self, exc_type, exc_value, traceback):
200209
self.close()
201210

202211
def __del__(self):
203-
if not Util.is_async_code:
204-
self.close()
212+
if not self._closed:
213+
unclosed_resource_warn(self)
214+
# TODO: 6.0 - remove this
215+
if not self._closed:
216+
if not Util.is_async_code:
217+
deprecation_warn(
218+
"Relying on Driver's destructor to close the session "
219+
"is deprecated. Please make sure to close the session. "
220+
"Use it as a context (`with` statement) or make sure to "
221+
"call `.close()` explicitly. Future versions of the "
222+
"driver will not close drivers automatically."
223+
)
224+
self.close()
205225

206226
@property
207227
def encrypted(self):
@@ -225,6 +245,7 @@ def close(self):
225245
""" Shut down, closing any open connections in the pool.
226246
"""
227247
self._pool.close()
248+
self._closed = True
228249

229250
@experimental("The configuration may change in the future.")
230251
def verify_connectivity(self, **config):

neo4j/_sync/work/session.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,22 +84,11 @@ class Session(Workspace):
8484
# The state this session is in.
8585
_state_failed = False
8686

87-
# Session have been properly closed.
88-
_closed = False
89-
9087
def __init__(self, pool, session_config):
9188
super().__init__(pool, session_config)
9289
assert isinstance(session_config, SessionConfig)
9390
self._bookmarks = self._prepare_bookmarks(session_config.bookmarks)
9491

95-
def __del__(self):
96-
if asyncio.iscoroutinefunction(self.close):
97-
return
98-
try:
99-
self.close()
100-
except (OSError, ServiceUnavailable, SessionExpired):
101-
pass
102-
10392
def __enter__(self):
10493
return self
10594

neo4j/_sync/work/workspace.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@
1919
import asyncio
2020

2121
from ...conf import WorkspaceConfig
22-
from ...exceptions import ServiceUnavailable
22+
from ...exceptions import (
23+
ServiceUnavailable,
24+
SessionExpired,
25+
)
26+
from ...meta import (
27+
deprecation_warn,
28+
unclosed_resource_warn,
29+
)
2330
from ..io import Neo4jPool
2431

2532

@@ -34,13 +41,25 @@ def __init__(self, pool, config):
3441
# Sessions are supposed to cache the database on which to operate.
3542
self._cached_database = False
3643
self._bookmarks = None
44+
# Workspace has been closed.
45+
self._closed = False
3746

3847
def __del__(self):
48+
if not self._closed:
49+
unclosed_resource_warn(self)
50+
# TODO: 6.0 - remove this
3951
if asyncio.iscoroutinefunction(self.close):
4052
return
4153
try:
54+
deprecation_warn(
55+
"Relying on Session's destructor to close the session "
56+
"is deprecated. Please make sure to close the session. Use it "
57+
"as a context (`with` statement) or make sure to call "
58+
"`.close()` explicitly. Future versions of the driver will "
59+
"not close sessions automatically."
60+
)
4261
self.close()
43-
except OSError:
62+
except (OSError, ServiceUnavailable, SessionExpired):
4463
pass
4564

4665
def __enter__(self):
@@ -100,3 +119,4 @@ def _disconnect(self, sync=False):
100119

101120
def close(self):
102121
self._disconnect(sync=True)
122+
self._closed = True

neo4j/meta.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,16 @@ def f_(*args, **kwargs):
9696
return f(*args, **kwargs)
9797
return f_
9898
return f__
99+
100+
101+
def unclosed_resource_warn(obj):
102+
import tracemalloc
103+
from warnings import warn
104+
msg = f"Unclosed {obj!r}."
105+
trace = tracemalloc.get_object_traceback(obj)
106+
if trace:
107+
msg += "\nObject allocated at (most recent call last):\n"
108+
msg += "\n".join(trace.format())
109+
else:
110+
msg += "\nEnable tracemalloc to get the object allocation traceback."
111+
warn(msg, ResourceWarning, stacklevel=2, source=obj)

0 commit comments

Comments
 (0)