Skip to content

Commit 4435183

Browse files
sap_hana: address review feedback on schema collection job
- cancel() now stops the schema collection job too, so its background thread and dedicated HANA connection are released promptly on teardown instead of lingering until the inactivity timeout. - Reset the job connection when a HANA error is swallowed inside collect_schemas(): the base SchemaCollector catches per-database errors and returns, so a transient disconnect never reached run_job's handler and the dead connection was reused every cycle. The collector now drops its connection reference on HanaError and the job reconnects next cycle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ec2ff5d commit 4435183

3 files changed

Lines changed: 64 additions & 14 deletions

File tree

sap_hana/datadog_checks/sap_hana/sap_hana.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,11 @@ def check(self, _):
159159
self._connection_flaked = False
160160

161161
def cancel(self):
162-
# Signal the Data Observability async job to stop so its executor thread is
163-
# released when the check is unscheduled (e.g. cluster-agent flavor or one-off
164-
# check invocations), instead of leaking the DBMAsyncJob thread pool.
162+
# Signal both async jobs to stop so their executor threads (and the schema job's
163+
# dedicated HANA connection) are released when the check is unscheduled (e.g.
164+
# cluster-agent flavor or one-off check invocations), instead of leaking the
165+
# DBMAsyncJob thread pool.
166+
self._schema_collection_job.cancel()
165167
self.data_observability.cancel()
166168

167169
def set_default_methods(self):

sap_hana/datadog_checks/sap_hana/schemas.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -281,21 +281,31 @@ def _get_databases(self) -> list[DatabaseInfo]:
281281
pass
282282
return [{'name': db_name, 'description': description}]
283283
except Exception as e:
284+
# A dead HANA connection surfaces here rather than propagating, so drop our
285+
# reference to signal the owning job to reconnect on the next cycle.
286+
if isinstance(e, HanaError):
287+
self._conn = None
284288
self._log.warning("Could not determine current HANA database; skipping schema collection: %s", e)
285289
return []
286290

287291
@contextlib.contextmanager
288292
def _get_cursor(self, _database_name):
289293
conn = self._active_conn()
290-
self._query_builder.ensure_stats_permission(conn)
291-
query, params = self._query_builder.build()
292-
with closing(conn.cursor()) as cursor:
293-
cursor.execute(query, params)
294-
self._pending_row = cursor.fetchone()
295-
try:
296-
yield cursor
297-
finally:
298-
self._pending_row = None
294+
try:
295+
self._query_builder.ensure_stats_permission(conn)
296+
query, params = self._query_builder.build()
297+
with closing(conn.cursor()) as cursor:
298+
cursor.execute(query, params)
299+
self._pending_row = cursor.fetchone()
300+
try:
301+
yield cursor
302+
finally:
303+
self._pending_row = None
304+
except HanaError:
305+
# collect_schemas() swallows this per-database error, so signal the dead
306+
# connection to the owning job by dropping our reference; it reconnects next cycle.
307+
self._conn = None
308+
raise
299309

300310
def _get_next(self, cursor):
301311
"""Assemble one table/view from consecutive cursor rows sharing the same (schema, table) key."""
@@ -399,6 +409,15 @@ def run_job(self) -> None:
399409
self._schema_collector._conn = conn
400410
self._schema_collector.collect_schemas()
401411
except HanaError:
402-
self._job_conn = None
403-
self._schema_collector._conn = None
412+
self._reset_conn()
404413
raise
414+
# collect_schemas() swallows per-database HANA errors internally, so a transient
415+
# disconnect never reaches the except above. The collector drops its connection
416+
# reference in that case; close ours too so the next cycle reconnects instead of
417+
# reusing a dead handle forever.
418+
if self._schema_collector._conn is None:
419+
self._reset_conn()
420+
421+
def _reset_conn(self) -> None:
422+
self._shutdown()
423+
self._schema_collector._conn = None

sap_hana/tests/test_schemas_diagnose.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,35 @@ def test_schema_job_resets_conn_on_hana_error(self):
230230
assert job._job_conn is None
231231
assert job._schema_collector._conn is None
232232

233+
def test_schema_job_resets_conn_on_swallowed_error(self):
234+
# collect_schemas() swallows per-database HANA errors and returns normally after
235+
# dropping the collector's connection reference; the job must still reconnect.
236+
check = _make_check({'collect_schemas': {'enabled': True, 'run_sync': True}})
237+
job = check._schema_collection_job
238+
conn = mock.MagicMock()
239+
job._job_conn = conn
240+
241+
def swallow():
242+
job._schema_collector._conn = None
243+
244+
job._schema_collector.collect_schemas = mock.MagicMock(side_effect=swallow)
245+
246+
job.run_job()
247+
248+
conn.close.assert_called_once()
249+
assert job._job_conn is None
250+
assert job._schema_collector._conn is None
251+
252+
def test_cancel_cancels_schema_collection_job(self):
253+
check = _make_check({'collect_schemas': {'enabled': True}})
254+
check._schema_collection_job.cancel = mock.MagicMock()
255+
check.data_observability.cancel = mock.MagicMock()
256+
257+
check.cancel()
258+
259+
check._schema_collection_job.cancel.assert_called_once()
260+
check.data_observability.cancel.assert_called_once()
261+
233262
def test_schema_job_disabled_does_not_run(self):
234263
check = _make_check()
235264
job = check._schema_collection_job

0 commit comments

Comments
 (0)