feat(ingest/gc): add query cleanup pass for stale SYSTEM queries#18120
feat(ingest/gc): add query cleanup pass for stale SYSTEM queries#18120aviraj-gour wants to merge 14 commits into
Conversation
Add a QueryCleanup module to datahub-gc that soft-deletes old SYSTEM queries selected by age alone (lastModifiedAt older than retention_days) via a single server-side search filter, with no reference gate. The existing SoftDeletedEntitiesCleanup completes the hard-delete second pass. Query entities previously had no first-pass cleanup and are excluded from stateful ingestion's stale-entity removal, so stale SYSTEM queries accumulated as permanent orphans in Elasticsearch and primary storage. - New query_cleanup.py (config, report, QueryCleanup) with run-bounding (limit_entities_delete, runtime_limit_seconds) and threaded deletes. - Wire query_cleanup into DataHubGcSource, ordered before the soft-deleted-entities cleanup stage. - Add query_cleanup block to the datahub-gc bootstrap template (v7). - Unit tests for filter selection, soft/hard delete, dry-run, and limits. Disabled by default (destructive). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Add a Query Cleanup section to the datahub-gc connector docs describing the SYSTEM-query soft-delete first pass, its config, and the retention_days guidance relative to the soft-deleted-entities cleanup clock. Also add a query_cleanup block to the starter recipe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Connector Tests ResultsAll connector tests passed for commit To skip connector tests, add the Autogenerated by the connector-tests CI pipeline. |
- Prefix report flags with qc_ to avoid MRO field collision with SoftDeletedEntitiesReport in DataHubGcSourceReport - Cap deletions at exactly limit_entities_delete (>= instead of >) - Drop redundant num_system_queries_found counter - Track invalid-URN skips in the report (num_queries_invalid_urn) - Document the deletion limit as an approximate cap under concurrency - Convert tests from unittest.TestCase to pytest idioms Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route soft deletes through the sink as StatusClass workunits (the same mechanism as stale-entity removal) instead of a bespoke ThreadPoolExecutor that called graph.delete_entity per URN. The sink batches and async-writes them (default ASYNC_BATCH), so query cleanup no longer maintains its own concurrency, backpressure, or emit-mode handling. Hard deletes have no bulk endpoint and are issued imperatively, one at a time. - Replace cleanup_queries() with a get_workunits() generator; datahub_gc now yields from it. - Drop max_workers, delay, futures_max_at_time, and emit_mode config, plus the futures/lock/_process_futures machinery they supported. Keep the runtime and deletion caps, enforced in the generator loop. - Report: drop num_queries_processed; add report_query_soft_deleted/ report_query_hard_deleted helpers; count at emit time; preview candidates in dry-run. Remove num_queries_invalid_urn in favor of report.warning, and count num_queries_found only for successfully parsed urns. - Update the datahub-gc docs and unit tests accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The workunit refactor removed max_workers from QueryCleanupConfig; the bootstrap template still injected it, which would fail config validation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hard delete has no bulk or async path, so the per-URN blocking calls are fanned out across a ThreadPoolExecutor (max_workers) instead of run serially. Completed futures are drained on the calling thread, so report updates need no lock. Soft delete stays workunit-based. Restores the max_workers config (and its bootstrap template entry) for the hard path only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atic failure Wrap the query hard-delete enumeration loop in try/finally so futures already submitted are still collected (and their completed deletes recorded in the report) when candidate enumeration raises mid-stream
Query cleanup now only soft-deletes stale SYSTEM queries and leaves the hard delete to the existing soft-deleted-entities cleanup pass, which also clears inbound references. Removes the hard_delete_entities/max_workers config, the worker-pool delete path, and the hard-deleted report counter, along with the corresponding docs and tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ch_size, limit_entities_delete, and runtime_limit_seconds
get_urns_by_filter issued scrollAcrossEntities with no sortInput, so candidates came back in arbitrary scroll order. When limit_entities_delete or runtime_limit_seconds tripped mid-stream, a capped run deleted an arbitrary subset rather than the oldest, and dry-run previews showed an arbitrary slice. Thread an optional sort_by/sort_order through get_urns_by_filter and have query cleanup request lastModifiedAt ascending so the most-stale queries drain first. The backend appends urn ascending as a tiebreaker, so search_after scroll pagination stays stable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78c2d56 to
7b160fc
Compare
… configs Remove hard_delete_entities/max_workers from the query_cleanup block of the datahub-gc bootstrap recipe; QueryCleanupConfig (extra="forbid") does not define them, so parsing the recipe would raise a ValidationError and crash the source. These were leftovers from the reverted hard-delete work. Also: document that limit_entities_delete=null disables the cap, use an explicit `is not None` guard, drop the always-truthy runtime_limit_seconds guard, update the dry_run and sort_order docstrings, and remove an unused import in the query cleanup test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump the default age cutoff for SYSTEM query soft-deletion from 30 to 90 days in QueryCleanupConfig and the datahub-gc bootstrap recipe, and update the connector doc examples to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| @@ -770,6 +771,7 @@ def test_get_urns_with_skip_cache() -> None: | |||
| "batchSize": unittest.mock.ANY, | |||
| "scrollId": None, | |||
| "skipCache": True, | |||
| "sortInput": None, | |||
There was a problem hiding this comment.
may we also add a test case with non-null sortInput ?
|
Reviewed the implementation — the approach is clean and well-scoped. Server-side selection ( Two things worth addressing: 1. The PR description doesn't match the shipped code. The description documents 2. Dry-run ignores Minor/optional: Generated by Claude Code |
sgomezvillamor
left a comment
There was a problem hiding this comment.
LGTM
As raised internally, just pending on final decision on default value in the bootstra mcp
Summary
Adds a
QueryCleanupfirst-pass to thedatahub-gcsource that soft-deletes oldSYSTEMqueries by age alone.Query entities have no first-pass cleanup today, and
queryis excluded from stateful ingestion's stale-entity removal. As a result, staleSYSTEMqueries accumulate as permanent orphans in Elasticsearch and primary storage, causing index bloat and search/UI noise.Approach
source == SYSTEMANDlastModifiedAt < cutoff(wherecutoff = now - retention_days).lastModifiedAtis already@SearchableonqueryProperties, so no backend change or reindex is needed.SYSTEM-only.MANUALqueries are never touched; this is enforced in the filter, not a config option.Status(removed=true)workunit — the same mechanism as stateful ingestion's stale-entity removal — and lets the sink batch and write it (the defaultdatahub-restsink mode isASYNC_BATCH). The source keeps no concurrency or emit-mode logic for this path.hard_delete_entitiesis set, there is no bulk or async hard-delete endpoint, so the per-URN blocking deletes are parallelized acrossmax_workersthreads. Completed futures are drained on the calling thread, so report updates need no locking. (This path skips reference cleanup — prefer the two-pass default.)SoftDeletedEntitiesCleanupalready listsqueryin its default entity types and performs the hard-delete second pass on a later run, additionally callingdeleteReferences.limit_entities_deleteorruntime_limit_secondsis reached, mirroringSoftDeletedEntitiesCleanup.Changes
metadata-ingestion/src/datahub/ingestion/source/gc/query_cleanup.py(QueryCleanupConfig,QueryCleanupReport,QueryCleanup).QueryCleanup.get_workunits()streams candidates and dispatches: dry-run preview, parallel hard delete, or soft-delete workunit emission.query_cleanupintoDataHubGcSource: config field, report base, instantiation, and a guarded stage thatyield froms the cleanup workunits, ordered before soft-deleted-entities cleanup.query_cleanupblock to theingestion-datahub-gc.yamlbootstrap template and bump its version tov7.metadata-ingestion/docs/sources/datahubgc/.metadata-ingestion/tests/unit/test_query_cleanup.py.Config
Disabled by default (
enabled: false) because it is destructive. Options:retention_days(30),hard_delete_entities(false — prefer the two-pass default),batch_size(500, search fetch size),max_workers(10, hard-delete parallelism only),limit_entities_delete(25000),runtime_limit_seconds(7200).Recommended rollout: enable with
dry_run: truefirst and inspectnum_queries_found/sample_deleted_queries, then flip to soft-delete with aretention_daysat least as large as your largest connector ingestion window (a live query'slastModifiedAtis refreshed on every re-observation, so only queries that have aged out of every ingestion window can cross the cutoff).Testing
./gradlew :metadata-ingestion:lintFix— clean (ruff + mypy).pytest tests/unit/test_query_cleanup.py— 10 passing: filter selection, soft-delete workunit emission, parallel hard delete, dry-run preview, deletion/time limits, disabled, and invalid-URN skip (reported as a warning).Checklist