fix: break health check import cycle#781
Conversation
Signed-off-by: Andre Manoel <amanoel@nvidia.com>
Greptile SummaryThis PR fixes an import cycle introduced by #750, where
|
| Filename | Overview |
|---|---|
| packages/data-designer/src/data_designer/interface/data_designer.py | Adds opt-in retry to check_models with validated max_attempts / retry_backoff_seconds and linear backoff; retry taxonomy (RETRYABLE_MODEL_ERRORS + TimeoutError) moved from the CI script into this public interface. |
| packages/data-designer-engine/src/data_designer/engine/readiness.py | Removes the now-unused client_concurrency_mode parameter and always uses the async path; ensure_async_engine_loop promoted to a top-level import. Logic is correct and cleaner. |
| scripts/health_checks.py | Simplified: no longer imports engine-internal RETRYABLE_MODEL_ERRORS or manages its own retry loop; delegates retry to DataDesigner.check_models. Correct and minimal. |
| packages/data-designer-engine/tests/engine/test_readiness.py | Replaces inline per-test async mocking with a consolidated autouse fixture; all assertions updated to arun_health_check. Coverage is complete and logically sound. |
| packages/data-designer/tests/interface/test_data_designer.py | New tests cover retryable errors, non-retryable early propagation, exhausted-attempt re-raise, sleep call sequencing, and input validation; fixtures refactored to shared scope. |
| packages/data-designer/tests/test_lazy_imports.py | New test_health_checks_entrypoint_imports_in_fresh_process test verifies the import cycle is resolved by running the script in a subprocess with all API keys removed and asserting exit code 0. |
| packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py | Two call sites of run_readiness_check drop the now-removed client_concurrency_mode kwarg and the associated import; straightforward cleanup. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Script as health_checks.py
participant DD as DataDesigner.check_models
participant RR as run_readiness_check
participant MR as ModelRegistry.arun_health_check
participant MCP as MCPRegistry.run_health_check
Script->>DD: "check_models(config, max_attempts=3, retry_backoff_seconds=5)"
loop attempt 1..max_attempts
DD->>RR: run_readiness_check(columns, resource_provider)
RR->>MR: arun_health_check(aliases) [via run_coroutine_threadsafe]
MR-->>RR: result / error
RR->>MCP: run_health_check(tool_aliases)
MCP-->>RR: ok / error
RR-->>DD: ok / retryable error / non-retryable error
alt non-retryable error
DD-->>Script: raise immediately
else retryable error AND attempts exhausted
DD-->>Script: raise
else retryable error AND attempts remain
DD->>DD: "time.sleep(attempt * backoff)"
else success
DD-->>Script: return
end
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Script as health_checks.py
participant DD as DataDesigner.check_models
participant RR as run_readiness_check
participant MR as ModelRegistry.arun_health_check
participant MCP as MCPRegistry.run_health_check
Script->>DD: "check_models(config, max_attempts=3, retry_backoff_seconds=5)"
loop attempt 1..max_attempts
DD->>RR: run_readiness_check(columns, resource_provider)
RR->>MR: arun_health_check(aliases) [via run_coroutine_threadsafe]
MR-->>RR: result / error
RR->>MCP: run_health_check(tool_aliases)
MCP-->>RR: ok / error
RR-->>DD: ok / retryable error / non-retryable error
alt non-retryable error
DD-->>Script: raise immediately
else retryable error AND attempts exhausted
DD-->>Script: raise
else retryable error AND attempts remain
DD->>DD: "time.sleep(attempt * backoff)"
else success
DD-->>Script: return
end
end
Reviews (4): Last reviewed commit: "Merge branch 'main' into andreatgretel/f..." | Re-trigger Greptile
Code Review: PR #781 —
|
nabinchha
left a comment
There was a problem hiding this comment.
Thanks for putting this together, @andreatgretel!
Summary
This restores the data_designer.engine.models.clients.create_model_client package export without eagerly importing the factory, which avoids the import cycle triggered when model retry errors are imported first. I agree this unblocks the health-check regression, but I think the current fix treats the symptom rather than the boundary issue that caused the cycle.
Findings
Warnings — Worth addressing
Design issues, missing error handling, test gaps, or violations of project standards that could cause problems later.
packages/data-designer-engine/src/data_designer/engine/models/clients/__init__.py:62 — Avoid using a lazy package export to compensate for a leaked engine boundary
- What: This PR fixes the cycle by making
create_model_clientlazy at theclientspackage boundary. The immediate trigger, though, is that the provider health-check script added in the previous PR importsdata_designer.engine.models.errors.RETRYABLE_MODEL_ERRORSdirectly so it can classify retries outside the public health-check/readiness path. - Why: That means a top-level CI script now knows about engine-internal model retry taxonomy. The lazy re-export is a reasonable facade pattern in isolation, but here it lets the cross-boundary dependency remain in place and makes import order part of the architecture. Future health-check callers could repeat the same pattern instead of relying on a single readiness contract.
- Suggestion: Could we move the retry policy behind the health-check boundary instead? For example,
DataDesigner.check_models(...),engine.readiness.run_readiness_check(...), or a small public helper could own retry classification and backoff. Thenscripts/health_checks.pycan keep calling the public interface without importingengine.models.errors, and we may not need to change theclientspackage export at all.
What Looks Good
- The fix keeps the public
from data_designer.engine.models.clients import create_model_clientsurface intact while deferring only the cycle-forming factory import. - The implementation follows the same
__getattr__/cache pattern already used bydata_designer.interfaceanddata_designer.config. - The new regression test exercises the import order in a fresh Python process, which is the right shape for circular-import coverage.
Verdict
Needs changes — I’d prefer we address the health-check retry boundary directly instead of adding a lazy export primarily to make the import cycle survivable.
This review was generated by an AI assistant.
Signed-off-by: Andre Manoel <amanoel@nvidia.com>
|
Fern preview: https://nvidia-preview-pr-781.docs.buildwithfern.com/nemo/datadesigner
|
|
Thanks for the review, @nabinchha. I pushed
CI is rerunning now. |
| run_readiness_check( | ||
| columns, | ||
| resource_provider, | ||
| client_concurrency_mode=ClientConcurrencyMode.ASYNC, |
There was a problem hiding this comment.
oh did we miss this in the PR to drop SYNC? we shouldn't need this distinction right?
There was a problem hiding this comment.
@nabinchha Yep, this was another leftover from #767. I pushed 9cac4619 to make readiness async-only: run_readiness_check now always uses arun_health_check, callers no longer pass a concurrency mode, and the obsolete sync-readiness test is removed.
I also did a repo-wide pass and found a few other sync-engine leftovers outside the readiness path. They do not affect this fix, so we’ll address them in a separate cleanup PR.
Local validation: engine 2170 passed; interface 988 passed, 1 skipped; Ruff clean.
📋 Summary
Restore provider health checks without exposing engine-internal retry taxonomy to the external CI script.
DataDesigner.check_modelsnow owns retry classification and backoff behind its public interface while preserving one-shot behavior by default.🔗 Related Issue
N/A - regression exposed by #750 and observed in Health Checks #48
🔄 Changes
DataDesigner.check_modelsscripts/health_checks.pyon the public interface with three attempts and 5s/10s delays🧪 Testing
.venv/bin/ruff check --fix ..venv/bin/ruff format ..venv/bin/pytest packages/data-designer/tests -q- 988 passed, 1 skipped✅ Checklist