fix(mssql): harden error handling and observability across throttling, description maps, and unguarded query paths - #31884
fix(mssql): harden error handling and observability across throttling, description maps, and unguarded query paths#31884akashverma0786 wants to merge 6 commits into
Conversation
…ilures, log Query Store choice per database - SQLSERVER_ERRORS now recognizes Azure SQL DTU/vCore throttling (10928/10929) instead of surfacing a generic, unclassified error. - Description-map load failures (schema/database/stored-procedure comments) now log at WARNING instead of silent DEBUG, so a real permission/connection issue is visible instead of disappearing. - The ingest-all-databases per-database engine loop now logs which query-log source (Query Store vs plan-cache DMVs) was picked for each database, matching the single-database path's existing log line.
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
| except Exception as exc: | ||
| logger.debug(traceback.format_exc()) | ||
| logger.debug(f"Could not load MSSQL {description_type} descriptions, continuing without them: {exc}") | ||
| logger.warning(f"Could not load MSSQL {description_type} descriptions, continuing without them: {exc}") |
There was a problem hiding this comment.
💡 Quality: WARNING on description-map failure may be noisy under ingestAllDatabases
Escalating the description-map load failure from DEBUG to WARNING (metadata.py:213) improves visibility, but under ingestAllDatabases an inaccessible system database (e.g. model, whose guest account is disabled by default) will emit a WARNING on every run for each of the three description types — the author's own manual testing confirmed 3 WARNING lines per run against model. This is an expected, benign condition being surfaced at WARNING, which risks alarm fatigue. Consider suppressing/downgrading to DEBUG for known-inaccessible system databases while keeping WARNING for genuine user-database failures, or including enough context in the message to make the expected-vs-unexpected distinction clear.
Was this helpful? React with 👍 / 👎
…g rule - pyodbc never exposes a SQL Server error number (see _mssql_number), so the number-only throttling rule left it undiagnosed. Add a text fallback, matching the pattern every other pyodbc-reachable rule in this file uses. - Add 10936 (the elastic-pool variant of 10928), missed in the original rule. - Verified 10928/10929/10936 against Microsoft's Azure SQL troubleshooting docs and a live sys.messages lookup; fixed the test fixtures to use the real message text instead of guessed/truncated wording.
"has been reached" was a generic enough substring match that any non-throttling SQL Server error mentioning a limit, on a pyodbc connection with no exposed error number, could misdiagnose as Azure SQL throttling. Match the specific "limit for the database is" / "limit for the elastic pool is" phrases from the real 10928/10936 messages instead - same coverage, narrower match.
✅ Playwright Results — workflow succeededValidated commit ✅ 110 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky PerformanceBlocking targets: ✅ met · Optimization targets: 🟡 in progress Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting. 🕒 Full workflow signal wall (to summary) 49m 40s ⏱️ Max setup 5m 53s · max shard execution 12m 43s · max shard-job elapsed before upload 19m 59s · reporting 3s 🌐 216.59 requests/attempt · 1.79 app boots/UI scenario · 0.00% common-shard skew Optimization targets still in progress:
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
…permission failures - _databases_to_scan() (query_parser.py) now falls back to the single configured engine instead of crashing the whole ingest-all-databases usage/lineage run when the database-listing query fails. - get_sqlalchemy_engine_dateformat() (utils.py) now returns None on failure so callers fall back to the already-documented DEFAULT_DATETIME_FORMAT instead of crashing source construction. - get_stored_procedures() (metadata.py) now reports a permission/syntax failure via status.warning() instead of letting it crash the whole generator uncaught - keeping it out of the success-rate calculation so one schema's stored-procedure permission gap doesn't fail the run. All three were reproduced live against a real SQL Server container via the actual metadata ingest/usage CLI before fixing, and re-verified live after. Adds matching unit tests.
The try/except wrap added in 48d2a51 re-indented the context.get().database / .database_schema accesses in get_stored_procedures(), shifting their column position enough to break the basedpyright baseline's match and surface them as new CI errors. The underlying reportAttributeAccessIssue is a longstanding, codebase- wide false positive (TopologyContext fields are injected dynamically at runtime via create_model()); suppress inline the same way snowflake/metadata.py already does for the identical case.
Code Review 👍 Approved with suggestions 1 resolved / 2 findingsImproves MSSQL connector reliability by classifying Azure SQL throttling errors, promoting description-map load failures to warnings, and logging per-database query-history source selection. Consider reviewing whether the description-map failure warning could become noisy during multi-database ingestion. 💡 Quality: WARNING on description-map failure may be noisy under ingestAllDatabases📄 ingestion/src/metadata/ingestion/source/database/mssql/metadata.py:211-213 Escalating the description-map load failure from DEBUG to WARNING (metadata.py:213) improves visibility, but under ✅ 1 resolved✅ Edge Case: Broad substring "has been reached" may misclassify errors
🤖 Prompt for agentsOptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
|



Describe your changes:
No linked issue — these are six independent reliability/observability hardening fixes for the mssql connector, found during a manual connector-reliability audit (error handling, fault tolerance, and observability standards). Happy to open issues retroactively if preferred.
Error classification & observability (3 commits, already merged into this branch):
SQLSERVER_ERRORSdidn't recognize Azure SQL's DTU/vCore resource-governance throttling errors (10928/10929/10936, including the pyodbc text-only fallback and elastic-pool wording), so a throttled customer got a generic/unclassified error instead of an actionable diagnosis.DEBUG, so a real permission or connection issue on that path was invisible by default. Raised toWARNING.ingestAllDatabasesper-database engine loop never logged which query-log source (Query Store vs plan-cache DMVs) was picked for each database, unlike the single-database path which already logs this.Unguarded query executions that could crash a run outright (3 new fixes, this commit):
_databases_to_scan()(query_parser.py) had no exception handling around the database-listing query that drivesingestAllDatabasesusage/lineage runs. A transient failure (network blip, timeout, deadlock — not permissions, see verification below) crashed the entire lineage/usage extraction instead of falling back to the single already-connected engine, which the code already does for the "zero databases returned" case.get_sqlalchemy_engine_dateformat()(utils.py) had no exception handling around itsDBCC USEROPTIONSprobe, so a transient connection failure at that exact moment crashedMssqlUsageSource/lineage source construction outright — even though a documentedDEFAULT_DATETIME_FORMATfallback sat right next to it, unused.get_stored_procedures()(metadata.py) had no exception handling around its listing query. A real, reproducible permission gap (DENY SELECT ON sys.sql_modules, which correlates with theVIEW DEFINITIONrequirement our public docs don't currently call out) crashed the whole generator uncaught, failing the entire workflow (WorkflowExecutionError) over one schema's stored procedures.I made them because each is a real, verified gap — see "Manual testing performed" below for how each was actually reproduced (not just read from the code) before being fixed, and re-verified live afterward. Two related findings from the same audit (
get_database_names's single-database branch, and_per_database_engines's engine-creation placement) were investigated with equal rigor and turned out to be false positives: live testing showed both are already fully covered bytest_connection()'s pre-flight checks and theHAS_DBACCESS/state=0filter already baked into the database-listing query, so no code change was made for those two.Type of change:
High-level design:
N/A — six small, independent changes, no shared design decisions:
ingestion/src/metadata/ingestion/source/database/mssql/connection.py: one newwhen()rule in the existingSQLSERVER_ERRORSErrorPack, tightened across two follow-up commits.ingestion/src/metadata/ingestion/source/database/mssql/metadata.py: one log-level change (debug→warning) for description maps;get_stored_procedures()'s listing query wrapped in try/except reporting viaself.status.warning()(notstatus.failed(), deliberately — see below).ingestion/src/metadata/ingestion/source/database/mssql/query_parser.py: one log line added per branch for Query Store selection;_databases_to_scan()wrapped in try/except falling back to the existing "no databases scanned" path.ingestion/src/metadata/ingestion/source/database/mssql/utils.py:get_sqlalchemy_engine_dateformat()wrapped in try/except, returningNone(its declaredOptional[str]already allowed this) so existing callers fall back to their documented default.One deliberate API choice worth calling out:
get_stored_procedures()'s failure path usesself.status.warning(key, reason), notself.status.failed(StackTraceError(...)).Status.calculate_success()only countsfailures, notwarnings— usingfailed()(which is what the siblingget_database_namesmulti-database branch already uses) would make a single schema's stored-procedure permission gap capable of trippingWorkflowExecutionErrorfor the whole run, which is disproportionate to what's actually lost.warning()keeps the gap visible in the pipeline status without failing the run. Verified live both ways (see manual testing).Tests:
Use cases covered
ingestAllDatabasesrun logs, per database, whether Query Store or plan-cache DMVs were used for query history.ingestAllDatabasesusage/lineage run degrades to the single configured engine instead of crashing the run.VIEW DEFINITION/sys.sql_modules) for one schema is reported as a warning and the rest of ingestion (databases, schemas, tables) completes normally, with the workflow still reporting overall success.Unit tests
ingestion/tests/unit/source/database/mssql/test_connection.py(3 tests, throttling classification, from the earlier commits),ingestion/tests/unit/topology/database/test_mssql.py(4 new tests this commit:test_get_stored_procedures_degrades_gracefully_on_query_failure,test_databases_to_scan_degrades_gracefully_on_query_failure,TestMssqlDateformatProbe::test_returns_dateformat_value/test_returns_none_when_probe_errors).pytest tests/unit/topology/database/test_mssql.py→ 52 passed (48 pre-existing + 4 new).ruff checkclean on all changed files.Backend integration tests
Ingestion integration tests
Playwright (UI) tests
Manual testing performed
For each of the three new fixes, reproduced the original crash live against a real SQL Server 2022 Docker container using the actual
metadata ingest/metadata usageCLI (not unit tests), then re-ran the identical scenario after the fix:get_stored_procedures()(highest-confidence repro): created a login, ranDENY SELECT ON sys.sql_modulesagainst it. Before fix:metadata ingestraisedWorkflowExecutionError, exit code 1, uncaughtOperationalError(SQL error 229) propagating out of the generator. After fix (first pass, usingstatus.failed): still exit code 1 — same disproportionate-failure problem, just with a cleaner message. After thestatus.warningcorrection:Warnings: 3,Success %: 100.0, exit code 0; databases/schemas/tables ingest normally._databases_to_scan(): tried to force a real permission-based failure onsys.databases(DENY SELECT ON sys.databases,DENY VIEW ANY DATABASE) — neither raises for a login with any database access; SQL Server filters rows there rather than denying the query. Confirmed the real trigger is transient/infra, not permissions. Regression-tested the fix live with a two-databaseingestAllDatabases: trueusage run — 100% success, both databases correctly listed.get_sqlalchemy_engine_dateformat(): confirmed live thatDBCC USEROPTIONSrequires zero permissions even for a heavily locked-down login, ruling out a permission-based repro; the only realistic trigger is a connection failure in the narrow window betweentest_connection()and this call. Verified the fix via deterministic code tracing (unconditional try/except) plus a live regression run (100% success, unaffected happy path).test_connection()beforeget_database_names()ever runs; and an inaccessible second database iningestAllDatabasesmode is silently and correctly excluded by the existingHAS_DBACCESS/state=0filter before_per_database_engines()ever sees it — confirmed with a live two-database run where the inaccessible one never appeared in any log or error.metadata usage/metadata ingestbaseline (happy-path) runs before and after every change to confirm no regressions — all at 100% success.UI screen recording / screenshots:
Not applicable.
Checklist:
Fixes <issue-number>: <short explanation>— no issue filed, see note above.Fixes #<issue-number>above — no issue filed, see note above.Note on merge order: this branch touches some of the same files (
metadata.py,query_parser.py,connection.py,test_mssql.py/test_connection.py) as other open mssql PRs (#31724, #31762, #31767). No two PRs edit the exact same lines as of this writing, but whichever merges last will need a routine rebase.Greptile Summary
The PR hardens the MSSQL connector’s error handling and observability.
Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains; the previously reported pyodbc throttling gap is addressed by text fallbacks that are reached through the production classifier.
Important Files Changed
Reviews (6): Last reviewed commit: "Merge branch 'main' into mssql-improveme..." | Re-trigger Greptile