Skip to content

Commit 4e724a0

Browse files
Move postgres async jobs onto the DatabaseCheck job registry (DataDog#24824)
* Move postgres async jobs onto the DatabaseCheck job registry Replace the hand-rolled _async_jobs property, _cancel_async_jobs helper and manual future/teardown loop with register_async_job, run_async_jobs, cancel_async_jobs and shutdown_async_jobs from DatabaseCheck. Co-authored-by: Cursor <cursoragent@cursor.com> * Add changelog Co-authored-by: Cursor <cursoragent@cursor.com> * Build postgres async jobs where they are registered Only construct the jobs the configuration enables, instead of building all four in __init__ and registering a subset. Unregistered jobs previously kept a back-reference to the check that shutdown_async_jobs() never cleared. Co-authored-by: Cursor <cursoragent@cursor.com> * Note that the job attributes only exist for tests Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 861bd5a commit 4e724a0

9 files changed

Lines changed: 89 additions & 42 deletions

File tree

postgres/changelog.d/24824.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Manage async job lifecycles through the ``DatabaseCheck`` job registry.

postgres/datadog_checks/postgres/data_observability.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def __init__(self, check: PostgreSql, config: InstanceConfig):
6060
# Filter bad queries on check construction.
6161
self._queries, self._schedulers = self._filter_valid_queries(self._do_config.queries or ())
6262

63-
def _shutdown(self):
63+
def shutdown(self) -> None:
6464
self._check = None
6565

6666
@property

postgres/datadog_checks/postgres/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def __init__(self, check: PostgreSql, config: InstanceConfig):
137137
self._tags_no_db = None
138138
self.tags = None
139139

140-
def _shutdown(self):
140+
def shutdown(self) -> None:
141141
self._check = None
142142
self._schema_collector = None
143143
self._column_statistics_collector = None

postgres/datadog_checks/postgres/postgres.py

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,13 @@ def __init__(self, name, init_config, instances):
164164
token_provider=self.build_token_provider(),
165165
)
166166
self.metrics_cache = PostgresMetricsCache(self._config)
167-
# Initialize statement metrics collector before server version is known.
168-
self.statement_metrics = PostgresStatementMetrics(self, self._config)
169-
self.statement_samples = PostgresStatementSamples(self, self._config)
170-
self.metadata_samples = PostgresMetadata(self, self._config)
171-
self.data_observability = PostgresDataObservability(self, self._config)
167+
# Only tests read these; the registry owns the jobs. Remove them once DatabaseCheck
168+
# exposes a public job accessor. Jobs the configuration does not enable stay None.
169+
self.statement_metrics = None
170+
self.statement_samples = None
171+
self.metadata_samples = None
172+
self.data_observability = None
173+
self._register_async_jobs()
172174
self._relations_manager = RelationsManager(self._config.relations, self._config.max_relations)
173175
self._clean_state()
174176
self._query_manager = QueryManager(self, lambda _: None, queries=[]) # query executor is set later
@@ -510,7 +512,7 @@ def cancel(self):
510512
cancel().
511513
"""
512514
self.log.debug("Marking check as cancelled")
513-
self._cancel_async_jobs()
515+
self.cancel_async_jobs()
514516
needs_finalize = False
515517
with self._cancel_lock:
516518
self._cancelled = True
@@ -522,31 +524,22 @@ def cancel(self):
522524
else:
523525
self.log.debug("cancel() deferred finalize, check is still running")
524526

525-
@property
526-
def _async_jobs(self):
527-
"""Return the async jobs active for this check's configuration."""
528-
jobs = []
527+
def _register_async_jobs(self):
528+
"""Build and register the async jobs enabled by this check's configuration."""
529529
if self._config.dbm:
530-
jobs.extend([self.statement_metrics, self.statement_samples, self.metadata_samples])
531-
elif self._config.data_observability.enabled:
532-
jobs.append(self.metadata_samples)
530+
# Built before the server version is known; _initialize_statement_metrics replaces it
531+
# with the collector that suits the version.
532+
self.statement_metrics = self.register_async_job(PostgresStatementMetrics(self, self._config))
533+
self.statement_samples = self.register_async_job(PostgresStatementSamples(self, self._config))
534+
if self._config.dbm or self._config.data_observability.enabled:
535+
self.metadata_samples = self.register_async_job(PostgresMetadata(self, self._config))
533536
if self._config.data_observability.enabled:
534-
jobs.append(self.data_observability)
535-
return jobs
536-
537-
def _cancel_async_jobs(self):
538-
"""Signal async jobs to stop. Safe to call while check() is running."""
539-
for job in self._async_jobs:
540-
job.cancel()
537+
self.data_observability = self.register_async_job(PostgresDataObservability(self, self._config))
541538

542539
def _finalize(self):
543540
"""Tear down check state. Must not run while check() is executing."""
544541
self.log.debug("Finalizing check: closing connections and clearing state")
545-
for job in self._async_jobs:
546-
if job._job_loop_future:
547-
job._job_loop_future.result()
548-
job._job_loop_future = None
549-
job._shutdown()
542+
self.shutdown_async_jobs()
550543
self._clean_state()
551544
self.check_initializations.clear()
552545
# TODO: move diagnosis cleanup into AgentCheck.cancel() in the base class
@@ -635,6 +628,8 @@ def load_version(self):
635628
self.set_metadata('version', self.raw_version)
636629

637630
def _initialize_statement_metrics(self):
631+
if not self._config.dbm:
632+
return
638633
custom_pgss_view = self._config.pg_stat_statements_view != 'pg_stat_statements'
639634
if self._config.query_metrics.incremental_query_metrics and self.version < V10:
640635
self.log.warning(
@@ -652,11 +647,14 @@ def _initialize_statement_metrics(self):
652647

653648
if self._config.query_metrics.incremental_query_metrics and self.version >= V10 and not custom_pgss_view:
654649
self.log.info("Using incremental query metrics collector")
655-
self.statement_metrics = PostgresStatementMetricsV2(self, self._config)
650+
collector = PostgresStatementMetricsV2(self, self._config)
656651
else:
657652
if not self._config.query_metrics.incremental_query_metrics:
658653
self.log.info("Using legacy query metrics collector (full pg_stat_statements load)")
659-
self.statement_metrics = PostgresStatementMetrics(self, self._config)
654+
collector = PostgresStatementMetrics(self, self._config)
655+
# Both collectors use the same job name, so registering replaces the instance built in
656+
# _register_async_jobs.
657+
self.statement_metrics = self.register_async_job(collector)
660658

661659
def initialize_is_aurora(self):
662660
if self.is_aurora is None:
@@ -1242,14 +1240,7 @@ def check(self, _):
12421240
if not self._config.only_custom_queries:
12431241
self._collect_stats(tags)
12441242
if not self._cancelled:
1245-
if self._config.dbm:
1246-
self.statement_metrics.run_job_loop(tags)
1247-
self.statement_samples.run_job_loop(tags)
1248-
self.metadata_samples.run_job_loop(tags)
1249-
elif self._config.data_observability.enabled:
1250-
self.metadata_samples.run_job_loop(tags)
1251-
if self._config.data_observability.enabled:
1252-
self.data_observability.run_job_loop(tags)
1243+
self.run_async_jobs(tags)
12531244
if self._config.collect_wal_metrics is True:
12541245
# collect wal metrics for pg < 10 only when explicitly enabled
12551246
# (requires local filesystem access to the WAL directory)

postgres/datadog_checks/postgres/statement_samples.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def __init__(self, check: PostgreSql, config: InstanceConfig):
231231
self._time_since_last_activity_event = 0
232232
self._pg_stat_activity_cols = None
233233

234-
def _shutdown(self):
234+
def shutdown(self) -> None:
235235
self._check = None
236236
self._explain_parameterized_queries = None
237237
self._collection_strategy_cache = None

postgres/datadog_checks/postgres/statements.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def __init__(self, check, config: InstanceConfig):
187187
ttl=60 * 60 / config.query_metrics.full_statement_text_samples_per_hour_per_query,
188188
)
189189

190-
def _shutdown(self):
190+
def shutdown(self) -> None:
191191
self._check = None
192192
self._full_statement_text_cache = None
193193
self._state = None

postgres/datadog_checks/postgres/statements_v2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def __init__(self, check, config: InstanceConfig):
134134
)
135135
self._stat_column_cache: list[str] = []
136136

137-
def _shutdown(self):
137+
def shutdown(self) -> None:
138138
self._check = None
139139
self._full_statement_text_cache = None
140140
self._delta_detector = None

postgres/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ classifiers = [
2828
"Private :: Do Not Upload",
2929
]
3030
dependencies = [
31-
"datadog-checks-base>=37.42.0",
31+
"datadog-checks-base>=38.0.0",
3232
]
3333
dynamic = [
3434
"version",

postgres/tests/test_unit.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
from datadog_checks.postgres import PostgreSql, util
1515
from datadog_checks.postgres.schemas import PostgresSchemaCollector
16+
from datadog_checks.postgres.statements import PostgresStatementMetrics
17+
from datadog_checks.postgres.statements_v2 import PostgresStatementMetricsV2
1618

1719
pytestmark = pytest.mark.unit
1820

@@ -409,7 +411,7 @@ def test_check_gc_after_cancel(pg_instance):
409411
2. Find which attribute on that object points back to the check (usually
410412
``self.check`` or ``self._check``).
411413
3. Null that attribute in ``cancel()`` or add it to the relevant
412-
``_shutdown()`` method.
414+
``shutdown()`` method.
413415
4. If the referrer is a closure or ``functools.partial``, find the
414416
registration site and null or clear the container that holds it.
415417
"""
@@ -517,6 +519,59 @@ def test_run_after_cancel_returns_immediately(pg_instance):
517519
assert result == ''
518520

519521

522+
@pytest.mark.parametrize(
523+
'dbm, data_observability_enabled, expected_jobs',
524+
[
525+
(False, False, []),
526+
(True, False, ['query-metrics', 'query-samples', 'database-metadata']),
527+
(False, True, ['database-metadata', 'data-observability']),
528+
(True, True, ['query-metrics', 'query-samples', 'database-metadata', 'data-observability']),
529+
],
530+
)
531+
def test_async_job_registry_matches_config(pg_instance, dbm, data_observability_enabled, expected_jobs):
532+
"""Only the jobs enabled by the instance config are built and registered."""
533+
pg_instance['dbm'] = dbm
534+
pg_instance['data_observability'] = {'enabled': data_observability_enabled}
535+
536+
check = PostgreSql('postgres', {}, [pg_instance])
537+
538+
registered = check._async_job_registry
539+
assert list(registered) == expected_jobs
540+
# Each attribute holds the registered job, or None when the config does not enable it.
541+
assert check.statement_metrics is registered.get('query-metrics')
542+
assert check.statement_samples is registered.get('query-samples')
543+
assert check.metadata_samples is registered.get('database-metadata')
544+
assert check.data_observability is registered.get('data-observability')
545+
546+
547+
def test_initialize_statement_metrics_replaces_registered_job(pg_instance):
548+
"""The incremental collector replaces the placeholder registered before the version was known."""
549+
pg_instance['dbm'] = True
550+
pg_instance['query_metrics'] = {'enabled': True, 'incremental_query_metrics': True}
551+
552+
check = PostgreSql('postgres', {}, [pg_instance])
553+
assert isinstance(check._async_job_registry['query-metrics'], PostgresStatementMetrics)
554+
555+
check.version = VersionInfo(14, 0, 0)
556+
check._initialize_statement_metrics()
557+
558+
assert isinstance(check.statement_metrics, PostgresStatementMetricsV2)
559+
assert check._async_job_registry['query-metrics'] is check.statement_metrics
560+
assert list(check._async_job_registry) == ['query-metrics', 'query-samples', 'database-metadata']
561+
562+
563+
def test_initialize_statement_metrics_noop_without_dbm(pg_instance):
564+
"""Without DBM there is no query metrics job to build."""
565+
pg_instance['dbm'] = False
566+
567+
check = PostgreSql('postgres', {}, [pg_instance])
568+
check.version = VersionInfo(14, 0, 0)
569+
check._initialize_statement_metrics()
570+
571+
assert check.statement_metrics is None
572+
assert check._async_job_registry == {}
573+
574+
520575
def test_collect_column_statistics_updates_timestamp_on_failure(pg_instance):
521576
pg_instance['dbm'] = True
522577
pg_instance['collect_column_statistics'] = {'enabled': True, 'collection_interval': 60}

0 commit comments

Comments
 (0)