Skip to content

Fixes #30696: feat(ingestion): enforce import layering and import-cost ceilings - #30697

Open
IceS2 wants to merge 6 commits into
mainfrom
feat/import-linter-enforcement
Open

Fixes #30696: feat(ingestion): enforce import layering and import-cost ceilings#30697
IceS2 wants to merge 6 commits into
mainfrom
feat/import-linter-enforcement

Conversation

@IceS2

@IceS2 IceS2 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #30696.

#30376 cut import metadata peak RSS by deferring the generated-model pydantic build. This adds the guardrails so the cost cannot grow back silently, and so the remaining lazy-import work has something to measure against.

What's added

Layering contractingestion/.importlinter declares a layering (generated < client < framework < ingestion.source < entrypoints) and import-linter fails when a module imports a higher layer, because that drags the higher layer's subtree into every process that imports the lower one.

The 56 existing violations are recorded in ignore_imports as the burn-down baseline, so today's tree passes. They are grouped by cause rather than annotated one by one: shared helpers (utils, mixins, config) that grew upwards, framework and connector types carried on the ometa client's mixins, feature packages bound to individual connectors, and ingestion.models spanning layers. Each entry was checked by removing it and confirming the contract then fails, so the list has nothing redundant in it.

Import-cost ceilingsingestion/tests/unit/test_import_cost.py caps import metadata peak RSS (480 MB) and module count (5000), measured in a subprocess. Deliberately loose: they catch a step change, not run-to-run noise. Verified they bite — with OM_PYDANTIC_DEFER_BUILD=0 (simulating a defer_build revert) the RSS test fails at 509 MB.

tests/unit/test_import_layers.py gates the contract in CI and additionally asserts root_packages stays exhaustive (see below).

PLC0415 is now ignored (import-outside-top-level). It made every deliberate lazy import carry a noqa, which is backwards here — function-local imports are the tool for keeping heavy subtrees out of import time. The 267 directives it required are removed (that is the bulk of the diff: comment-only, 117 files).

Configuring import-linter for this package

root_packages has to name every PEP 420 namespace portion under metadata explicitly. metadata itself is a regular package, and grimp does not descend into namespace directories nested inside one — so naming only metadata analyses 166 modules, while naming the portions analyses 2404. Those subpackages are namespace packages deliberately: the Collate ingestion-extension merges connectors into metadata.ingestion.source.* from a separate distribution, so adding __init__.py is not an option.

Because that is easy to get wrong, test_every_metadata_subpackage_is_analysed fails if a new top-level subpackage is neither declared nor explicitly excluded.

Layer siblings are joined with : (not |) so modules within a layer may import each other.

Function-local imports still count. import-linter treats a deferred import as a dependency, which is what we want: making an import lazy hides its cost, not the dependency direction, so it cannot be used to get around a layer. Import cost is a separate concern, measured by the ceilings above.

Verification

  • make check_import_layers passes (Contracts: 1 kept, 0 broken, 2404 files / 10861 dependencies analysed). Adding a metadata.timer -> metadata.ingestion.source import fails it with the rule and the offending edge (exit 1); removing it passes (exit 0).
  • 4 tests pass across the new files; ruff check and ruff format --check clean over ingestion/.
  • Full tests/unit/{models,connections,core,utils} slice: 1 failed, 457 passed — identical counts on a clean tree, so the one failure (test_status_warning_handler.py::TestStepHandlerAttachment::test_warning_inside_run_scope_populates_status, order-dependent) is pre-existing and unrelated.

Follow-ups this unblocks

Making the SQL-lineage parser import lazy, string-dispatching the secrets-manager backends, and moving driver/profiler imports into functions — each removes baseline entries and shows up against the ceilings.

…t ceilings

Phase 0 (#30376) cut `import metadata` peak RSS with defer_build, but nothing
stops it growing back: it went from ~261 MB (1.9) to ~435 MB (1.13) unnoticed and
OOM-killed 512Mi automation pods. Add the guardrails.

scripts/check_import_layers.py enforces a layering (leaf < client < framework <
connectors < entrypoints): a module must not import a higher layer at module
scope, because that drags the higher layer's subtree into every process. The 94
existing violations are recorded in import_layers_baseline.json so they can be
burned down without regressing; the check fails on new violations and on stale
baseline entries. tests/unit/test_import_cost.py caps `import metadata` peak RSS
and module count, and tests/unit/test_import_layers.py gates the contract in CI.

Analysing the AST rather than import-linter is deliberate: most metadata.*
subpackages are PEP 420 namespace packages (Collate's ingestion-extension merges
connectors into them from a separate distribution), which grimp cannot see, and
only module-scope imports cost import time whereas grimp also counts
function-local ones.

Stop treating lazy imports as a smell: PLC0415 is now ignored, since
function-local imports are the tool for keeping heavy subtrees out of import
time, and the 267 noqa directives it required are removed.
Copilot AI review requested due to automatic review settings July 30, 2026 10:42
@IceS2
IceS2 requested a review from a team as a code owner July 30, 2026 10:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

✅ PR checks passed

The linked issue has a description and all required Shipping project fields set. Thanks!

@github-actions github-actions Bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels Jul 30, 2026
Comment thread ingestion/scripts/check_import_layers.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (141 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@github-actions

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit ac597b29a6b68f53b0a88aa58b35d414b36cfc4e in Playwright run 30557607941, attempt 1.

✅ 597 passed · ❌ 0 failed · 🟡 2 flaky · ⏭️ 3 skipped · 🧰 0 lifecycle flaky

Performance

Blocking 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) 50m 46s

⏱️ Max setup 2m 55s · max shard execution 14m 28s · max shard-job elapsed before upload 21m 36s · reporting 6s

🌐 209.12 requests/attempt · 2.77 app boots/UI scenario · 5.99% common-shard skew

Optimization targets still in progress:

  • Browser traffic was 209.12 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.77 per UI scenario (1748 boots / 632 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
🟡 Shard chromium-01 85 0 1 0 0 0
✅ Shard chromium-02 99 0 0 0 0 0
✅ Shard chromium-03 105 0 0 3 0 0
🟡 Shard chromium-04 108 0 1 0 0 0
✅ Shard data-asset-rules-01 61 0 0 0 0 0
✅ Shard domain-isolation-01 14 0 0 0 0 0
✅ Shard global-state-01 23 0 0 0 0 0
✅ Shard ingestion-01 32 0 0 0 0 0
✅ Shard ingestion-02 29 0 0 0 0 0
✅ Shard reindex-01 2 0 0 0 0 0
✅ Shard search-01 10 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 2 flaky test(s) (passed on retry)
  • Pages/Entity.spec.tsTier Add, Update and Remove (shard chromium-01, 1 retry)
  • Pages/Entity.spec.tsDomain Propagation (shard chromium-04, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Replaces the hand-rolled AST checker with import-linter, which does the job
properly: it analyses the same 2404 modules, reports the indirect chain behind a
violation rather than just the direct edge, and is configuration rather than code
we maintain.

The earlier conclusion that import-linter could not see this tree was wrong. It
can, as long as root_packages names every PEP 420 namespace portion under
metadata explicitly — metadata itself is a regular package and grimp does not
descend into namespace directories nested inside one. Naming only `metadata`
finds 166 modules; naming the portions finds 2404.

Siblings within a layer are joined with ':' so they may import each other. The 60
existing violations are recorded in ignore_imports as the burn-down baseline;
tests/unit/test_import_layers.py gates the contract and also asserts
root_packages stays exhaustive, since an unlisted namespace portion would be
silently unanalysed.

Note the tradeoff: import-linter counts function-local imports as edges, so the
baseline includes imports that are already deliberately lazy (for example
custom_pydantic -> secrets_manager_factory, which is function-local to break a
cycle). Import *cost* is measured separately by tests/unit/test_import_cost.py.
Copilot AI review requested due to automatic review settings July 30, 2026 12:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread ingestion/tests/unit/test_import_layers.py
@github-actions

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

Review of the contract turned up four things.

The ignore list was not minimal: five entries were only chain starts, and are
covered by ignoring the edge that actually crosses a layer. Each remaining entry
was checked by removing it and confirming the contract then fails, so the list is
now 56 entries with nothing redundant.

metadata.ingestion.progress and metadata.ingestion.diagnostics sat outside every
layer, which left imports to and from them unconstrained. Layering them surfaced a
real chain (progress -> models.topology -> ingestion.source) that was previously
invisible. Only metadata.ingestion.models stays unlayered, because
custom_pydantic belongs below metadata.generated while the rest of the package is
framework-level; test_every_ingestion_subpackage_has_a_layer now fails if any
other subpackage drifts out of a layer.

The ignore list mixed two different things: imports that are already
function-local, which import-linter cannot recognise and which will never be
removed, and module-scope imports that should be. They are now separate sections,
so the second one reads as the list of work it is.

Comments and docstrings carried release-by-release RSS figures and incident
detail that belongs in the issue, not in the tree; they are cut back to the
contract and the reason a reader would need.
Copilot AI review requested due to automatic review settings July 30, 2026 13:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

Declare metadata as a container so layers read as `ingestion.source` rather than
`metadata.ingestion.source`; each layer now fits on one line.

Group ignore_imports by why the import exists rather than listing 56 lines flat:
imports that are already lazy, shared helpers that grew upwards, the ometa
client carrying framework types, feature packages bound to individual connectors,
and ingestion.models spanning layers. Five headings instead of a comment per
entry. The entry set is unchanged.

Restore the instruction that the list is fixed by removing entries, not extended,
which was lost when the sections were introduced.
Copilot AI review requested due to automatic review settings July 30, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

The ignore list had a section for imports that are already function-local,
described as permanent because they cost nothing at import time. That was the
wrong distinction: deferring an import hides its cost, not the dependency, so a
function-local import across a layer is still the same design problem.

Those seven entries move into the groups they belong to by cause. All 56 now read
as one list of debt to remove.
Copilot AI review requested due to automatic review settings July 30, 2026 15:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

…change

py_format_check lints ingestion/ and openmetadata-airflow-apis/ together, so
ignoring PLC0415 left 80 unused noqa directives in the second tree and RUF100
failed the build.
Copilot AI review requested due to automatic review settings July 30, 2026 15:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitar-bot

gitar-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Enforces import layering and import-cost ceilings using import-linter and subprocess memory caps, addressing the layer checker under-detects and coverage test findings.

✅ 2 resolved
Edge Case: Layer checker under-detects some module-scope import forms

📄 ingestion/scripts/check_import_layers.py:101-115
module_scope_imports() silently ignores three import-time constructs, so a module-scope import of a higher layer written this way would escape the contract (the check fails open): (1) relative imports — node.level == 0 filters out any from . import X / from ..pkg import X at module scope (a handful exist, e.g. the hive metastore_dialects __init__ files); (2) the else: branch of if TYPE_CHECKING:_is_type_checking_guard matches and continue skips the whole node, but the else branch executes at import time; (3) match/case bodies, since cases is not among the walked attributes. Impact is limited (dev tooling, permissive failure), but the docstring claims to catch all import-time imports. Consider resolving relative imports against the current package, walking orelse of TYPE_CHECKING guards, and adding match case bodies to the descent.

Edge Case: Coverage test only guards top-level metadata subpackages

📄 ingestion/tests/unit/test_import_layers.py:54-68
test_every_metadata_subpackage_is_analysed only iterates direct children of the metadata package root, but the PR's whole rationale is that grimp does not descend into PEP 420 namespace directories nested inside a package (e.g. the connector distributions merged into metadata.ingestion.source.*). A newly added deeply-nested namespace subpackage would be silently unanalysed by the contract, and — having no baseline entry — would not trip import-linter's unmatched-ignore error either, so this test would not catch it despite the docstring claiming it prevents subpackages being 'invisible to the contract.' Consider walking nested namespace portions (or documenting that the guarantee is limited to the top level) so the guard matches the failure mode it describes.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar | Powered by Gitar — free for open source

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ingestion safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ingestion: guard import cost with a layer contract and RSS/module ceilings

2 participants