Skip to content

Commit ef3e2d0

Browse files
committed
Fix db connection leaks
1 parent 608e079 commit ef3e2d0

4 files changed

Lines changed: 39 additions & 14 deletions

File tree

python/rcdb/cli/db.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def db_command(ctx):
2424
print("ERROR connection string is missing.")
2525
exit(1)
2626
provider.connect(connection_str, check_version=False)
27+
ctx.call_on_close(provider.disconnect)
2728
query = select(SchemaVersion).order_by(SchemaVersion.version.desc())
2829
schema_version, = provider.session.execute(query).first()
2930
print("Schema version: {} - '{}'".format(schema_version.version, schema_version.comment))
@@ -88,6 +89,7 @@ def _print_sqlite_table_sizes(engine):
8889
@pass_rcdb_context
8990
def update(context):
9091
provider = RCDBProvider(context.connection_str, check_version=False)
92+
click.get_current_context().call_on_close(provider.disconnect)
9193

9294
# Check something exists
9395
if not sqlalchemy.inspect(provider.engine).has_table(SchemaVersion.__tablename__):
@@ -117,6 +119,7 @@ def update(context):
117119
return
118120

119121
provider = RCDBProvider(context.connection_str, check_version=False)
122+
click.get_current_context().call_on_close(provider.disconnect)
120123

121124
# Create alias table
122125
Alias.__table__.create(provider.engine)
@@ -181,6 +184,7 @@ def init(context, drop_all, no_defaults, confirm, add_tests, add_cpp_tests):
181184
# That we will need for DB
182185
metadata = rcdb.model.Base.metadata
183186
provider = RCDBProvider(context.connection_str, check_version=False)
187+
click.get_current_context().call_on_close(provider.disconnect)
184188

185189
# Drop all if needed
186190
if drop_all:

python/rcdb/cli/rp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def add(context, name, description, run_min, run_max, start_date, end_date):
5555

5656
provider = RCDBProvider()
5757
provider.connect(connection_str, check_version=False)
58+
click.get_current_context().call_on_close(provider.disconnect)
5859
session = provider.session
5960

6061
# Convert date strings to Date objects if provided
@@ -104,6 +105,7 @@ def rm(context, period_id, yes):
104105

105106
provider = RCDBProvider()
106107
provider.connect(connection_str, check_version=False)
108+
click.get_current_context().call_on_close(provider.disconnect)
107109
session = provider.session
108110

109111
rp_item = session.query(RunPeriod).filter(RunPeriod.id == period_id).one_or_none()
@@ -142,6 +144,7 @@ def update(context, period_id, name, description, run_min, run_max, start_date,
142144

143145
provider = RCDBProvider()
144146
provider.connect(connection_str, check_version=False)
147+
click.get_current_context().call_on_close(provider.disconnect)
145148
session = provider.session
146149

147150
rp_item = session.query(RunPeriod).filter(RunPeriod.id == period_id).one_or_none()

python/rcdb/provider.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,28 @@ def connect(self, connection_string="mysql+pymysql://rcdb@127.0.0.1/rcdb", check
133133
self._connection_string = connection_string
134134

135135
if check_version:
136-
db_version = self.get_schema_version()
137-
if db_version != rcdb.SQL_SCHEMA_VERSION:
138-
message = "SQL schema version doesn't match. " \
139-
"Retrieved DB version is {0}, required version is {1}. " \
140-
.format(db_version, rcdb.SQL_SCHEMA_VERSION)
141-
if db_version is not None and db_version < rcdb.SQL_SCHEMA_VERSION:
142-
message += "The database schema is older than this RCDB version. " \
143-
"Run 'rcdb db update' to migrate the database to the current schema."
144-
else:
145-
message += "The database schema is newer than this RCDB version. " \
146-
"Update your RCDB installation to match the database."
147-
raise rcdb.errors.SqlSchemaVersionError(message)
136+
# This version query is the first statement that actually opens a
137+
# DB connection. If it fails (unreachable host, access denied, wrong
138+
# schema, ...) the engine/session we just built must be disposed
139+
# here: the exception propagates out of connect()/__init__, so the
140+
# caller never receives this provider to disconnect() it themselves,
141+
# and the engine's open connection would otherwise leak until GC.
142+
try:
143+
db_version = self.get_schema_version()
144+
if db_version != rcdb.SQL_SCHEMA_VERSION:
145+
message = "SQL schema version doesn't match. " \
146+
"Retrieved DB version is {0}, required version is {1}. " \
147+
.format(db_version, rcdb.SQL_SCHEMA_VERSION)
148+
if db_version is not None and db_version < rcdb.SQL_SCHEMA_VERSION:
149+
message += "The database schema is older than this RCDB version. " \
150+
"Run 'rcdb db update' to migrate the database to the current schema."
151+
else:
152+
message += "The database schema is newer than this RCDB version. " \
153+
"Update your RCDB installation to match the database."
154+
raise rcdb.errors.SqlSchemaVersionError(message)
155+
except Exception:
156+
self.disconnect()
157+
raise
148158

149159
# ------------------------------------------------
150160
# Closes connection to data
@@ -164,6 +174,11 @@ def disconnect(self):
164174
if self.engine is not None:
165175
self.engine.dispose()
166176

177+
# ``close`` is an alias for ``disconnect``: a lot of calling code (and other
178+
# resource-holding objects) expects a ``close()`` method, while existing
179+
# RCDB code and users rely on the historical ``disconnect()`` name. Keep both.
180+
close = disconnect
181+
167182
# -------------------------------------------------
168183
# indicates ether the connection is open or not
169184
# -------------------------------------------------

python/rcdb/web/modules/runs.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,11 @@ def elog(run_number):
157157
from urllib.request import urlopen
158158
from urllib.error import HTTPError
159159
try:
160-
elog_json = urlopen('https://logbooks.jlab.org/api/elog/entries?book=hdrun&title=Run_{}&limit=1'
161-
.format(run_number)).read()
160+
# Context-manage the response so the underlying socket is closed
161+
# deterministically instead of leaking until GC.
162+
with urlopen('https://logbooks.jlab.org/api/elog/entries?book=hdrun&title=Run_{}&limit=1'
163+
.format(run_number)) as response:
164+
elog_json = response.read()
162165
except HTTPError as e:
163166
return jsonify(stat=str(e.code))
164167

0 commit comments

Comments
 (0)