Skip to content

Commit 0f40ac5

Browse files
Merge pull request #566 from Backblaze/close-sqlite-connections
Close sqlite connections in SqliteAccountInfo
2 parents 0340784 + 1df8435 commit 0f40ac5

File tree

6 files changed

+50
-41
lines changed

6 files changed

+50
-41
lines changed

b2sdk/_internal/account_info/sqlite_account_info.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import stat
1818
import sys
1919
import threading
20+
from contextlib import contextmanager
2021

2122
from .exception import CorruptAccountInfo, MissingAccountData
2223
from .upload_url_pool import UrlPoolAccountInfo
@@ -152,7 +153,7 @@ def _validate_database(self, last_upgrade_to_run=None):
152153

153154
# If we can connect to the database, and do anything, then all is good.
154155
try:
155-
with self._connect() as conn:
156+
with self._temp_connection() as conn:
156157
self._create_tables(conn, last_upgrade_to_run)
157158
return
158159
except sqlite3.DatabaseError:
@@ -178,7 +179,7 @@ def _validate_database(self, last_upgrade_to_run=None):
178179
# create a database
179180
self._create_database(last_upgrade_to_run)
180181
# add the data from the JSON file
181-
with self._connect() as conn:
182+
with self._temp_connection() as conn:
182183
self._create_tables(conn, last_upgrade_to_run)
183184
insert_statement = """
184185
INSERT INTO account
@@ -214,6 +215,24 @@ def _get_connection(self):
214215
def _connect(self):
215216
return sqlite3.connect(self.filename, isolation_level='EXCLUSIVE')
216217

218+
@contextmanager
219+
def _temp_connection(self):
220+
"""A one-off sqlite connection for setup purposes, that is not cached in thread_local."""
221+
conn = self._connect()
222+
try:
223+
with conn:
224+
yield conn
225+
finally:
226+
conn.close()
227+
228+
def close(self):
229+
connection = getattr(self.thread_local, 'connection', None)
230+
if connection is None:
231+
return
232+
233+
connection.close()
234+
del self.thread_local.connection
235+
217236
def _create_database(self, last_upgrade_to_run):
218237
"""
219238
Make sure that the database is created and has appropriate file permissions.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix `SqliteAccountInfo` to explicitly close temporary sqlite connections used during setup, add a `close()` method for cached connection.

test/integration/test_raw_api.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import re
1616
import sys
1717
import time
18-
import traceback
1918
from typing import List
2019

2120
import pytest
@@ -70,12 +69,8 @@ def test_raw_api(dont_cleanup_old_buckets):
7069

7170
print()
7271

73-
try:
74-
raw_api = B2RawHTTPApi(B2Http())
75-
raw_api_test_helper(raw_api, not dont_cleanup_old_buckets)
76-
except Exception:
77-
traceback.print_exc(file=sys.stdout)
78-
pytest.fail('test_raw_api failed')
72+
raw_api = B2RawHTTPApi(B2Http())
73+
raw_api_test_helper(raw_api, not dont_cleanup_old_buckets)
7974

8075

8176
def authorize_raw_api(raw_api):

test/unit/account_info/fixtures.py

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
from apiver_deps import InMemoryAccountInfo, SqliteAccountInfo
1414
from pytest_lazy_fixtures import lf
1515

16-
from b2sdk.v2 import SqliteAccountInfo as V2SqliteAccountInfo
17-
from b2sdk.v3 import SqliteAccountInfo as V3SqliteAccountInfo
16+
_USE_TMPDIR = object()
1817

1918

2019
@pytest.fixture
@@ -69,32 +68,22 @@ def in_memory_account_info(in_memory_account_info_factory):
6968

7069
@pytest.fixture
7170
def sqlite_account_info_factory(tmpdir):
72-
def get_account_info(file_name=None, last_upgrade_to_run: int | None = None):
73-
if file_name is None:
74-
file_name = str(tmpdir.join('b2_account_info'))
75-
return SqliteAccountInfo(file_name, last_upgrade_to_run)
76-
77-
return get_account_info
71+
created_account_infos = []
7872

79-
80-
@pytest.fixture
81-
def v2_sqlite_account_info_factory(tmpdir):
82-
def get_account_info(file_name=None):
83-
if file_name is None:
73+
def get_account_info(
74+
file_name=_USE_TMPDIR, *, account_info_class=SqliteAccountInfo, **account_info_kwargs
75+
):
76+
if file_name is _USE_TMPDIR:
8477
file_name = str(tmpdir.join('b2_account_info'))
85-
return V2SqliteAccountInfo(file_name, last_upgrade_to_run=4)
8678

87-
return get_account_info
79+
account_info = account_info_class(file_name=file_name, **account_info_kwargs)
80+
created_account_infos.append(account_info)
81+
return account_info
8882

83+
yield get_account_info
8984

90-
@pytest.fixture
91-
def v3_sqlite_account_info_factory(tmpdir):
92-
def get_account_info(file_name=None):
93-
if file_name is None:
94-
file_name = str(tmpdir.join('b2_account_info'))
95-
return V3SqliteAccountInfo(file_name)
96-
97-
return get_account_info
85+
for account_info in created_account_infos:
86+
account_info.close()
9887

9988

10089
@pytest.fixture

test/unit/account_info/test_account_info.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,8 @@ class TestSqliteAccountInfo(AccountInfoBase):
349349
PERSISTENCE = True
350350

351351
@pytest.fixture(autouse=True)
352-
def setUp(self, request):
352+
def setUp(self, request, sqlite_account_info_factory):
353+
self.sqlite_account_info_factory = sqlite_account_info_factory
353354
self.db_path = tempfile.NamedTemporaryFile(
354355
prefix=f'tmp_b2_tests_{request.node.name}__', delete=True
355356
).name
@@ -378,9 +379,7 @@ def test_permissions(self):
378379
"""
379380
Test that a new database won't be readable by just any user
380381
"""
381-
SqliteAccountInfo(
382-
file_name=self.db_path,
383-
)
382+
self.sqlite_account_info_factory(file_name=self.db_path)
384383
mode = os.stat(self.db_path).st_mode
385384
assert stat.filemode(mode) == '-rw-------'
386385

@@ -428,7 +427,7 @@ def _make_sqlite_account_info(self, env=None, last_upgrade_to_run=None):
428427
"""
429428
# Override HOME to ensure hermetic tests
430429
with mock.patch('os.environ', env or {'HOME': self.test_home}):
431-
return SqliteAccountInfo(
430+
return self.sqlite_account_info_factory(
432431
file_name=self.db_path if not env else None,
433432
last_upgrade_to_run=last_upgrade_to_run,
434433
)

test/unit/account_info/test_sqlite_account_info.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
)
2323
from apiver_deps_exception import MissingAccountData
2424

25+
from b2sdk.v2 import SqliteAccountInfo as V2SqliteAccountInfo
26+
from b2sdk.v3 import SqliteAccountInfo as V3SqliteAccountInfo
27+
2528
from .fixtures import *
2629

2730

@@ -194,8 +197,7 @@ def test_migrate_to_5_v3(
194197
@pytest.mark.apiver(2)
195198
def test_multi_bucket_key_error_apiver_v2(
196199
self,
197-
v2_sqlite_account_info_factory,
198-
v3_sqlite_account_info_factory,
200+
sqlite_account_info_factory,
199201
account_info_default_data,
200202
):
201203
allowed = dict(
@@ -204,13 +206,17 @@ def test_multi_bucket_key_error_apiver_v2(
204206
namePrefix=None,
205207
)
206208

207-
v3_account_info = v3_sqlite_account_info_factory()
209+
v3_account_info = sqlite_account_info_factory(account_info_class=V3SqliteAccountInfo)
208210
account_info_default_data.update({'allowed': allowed})
209211
v3_account_info.set_auth_data(**account_info_default_data)
210212

211213
assert v3_account_info.get_allowed() == allowed
212214

213-
v2_account_info = v2_sqlite_account_info_factory(file_name=v3_account_info.filename)
215+
v2_account_info = sqlite_account_info_factory(
216+
file_name=v3_account_info.filename,
217+
account_info_class=V2SqliteAccountInfo,
218+
last_upgrade_to_run=4,
219+
)
214220
with pytest.raises(MissingAccountData):
215221
v2_account_info.get_allowed()
216222

0 commit comments

Comments
 (0)