Repository navigation
Fix three ADF -> Databricks control-flow / codegen bugs - #151
alexnastetsky wants to merge 1 commit into
Conversation
Port of databricks-field-eng/flowx#17 onto the restructured codebase. 1. ForEach inputs-bridge notebooks carry the `# Databricks notebook source` marker. Already fixed on main by the render_bridge_notebook consolidation; this only adds a guard test. 2. A join after an IfCondition/Switch uses run_if NONE_FAILED so a failure in the taken branch propagates (AT_LEAST_ONE_SUCCESS let a sibling success mask it). 3. Synthesised _init_<var> tasks that nothing references are pruned. The translator records the exact synthesised keys on the Pipeline IR (serialised in translation_report.json), and pruning runs at the end of prepare_workflow -- after the agentic merge and after the preparers have materialised references the IR never shows (AppendVariable's source_task_key, ForEach inner-job params, taskValues reads in generated notebooks). Only structured reference forms count, so an unrelated key/path containing the key text never keeps a dead init alive. Pruned inits also drop their notebook and setup tasks. Co-authored-by: Isaac <no-reply@databricks.com>
ghanse
left a comment
There was a problem hiding this comment.
Looks good overall. The join fix and the report round-trip are solid. One gap in pruning plus a few test holes are noted inline.
| for task in _iter_workflow_tasks(tasks, artifacts.inner_workflows) | ||
| ] | ||
| dead_keys: set[str] = set() | ||
| for key in init_keys: |
There was a problem hiding this comment.
This is a single pass, so a dead chain only loses its tail. If _init_b reads _init_a and nothing reads _init_b, _init_b is pruned but _init_a stays, because _init_b still pointed at it. Could we loop until nothing new is dead, or work out what's reachable from the real tasks first?
| assert _find_dead(tasks, {"_init_runMode"}, inner_workflows=(inner,)) == frozenset() | ||
|
|
||
|
|
||
| def test_find_dead_keeps_init_read_by_another_init() -> None: |
There was a problem hiding this comment.
This covers a chain where every init is used. Can we add one where the whole chain is unused (_init_b reads _init_a, nothing reads _init_b) and assert both get pruned?
| ] | ||
|
|
||
|
|
||
| def test_condition_join_uses_none_failed_not_at_least_one_success() -> None: |
There was a problem hiding this comment.
Nit: only IfCondition is covered. A Switch case, plus a check that a non-join task keeps its run_if, would pin the scope down.
| assert _find_dead(tasks, {"_init_items"}) == frozenset() | ||
|
|
||
|
|
||
| def test_find_dead_keeps_init_read_from_notebook_code() -> None: |
There was a problem hiding this comment.
Nit: _notebook_references also matches taskKey="...", but only single quotes are tested here. Worth adding the double-quote case.
| return any(notebook.relative_path.endswith(f"/{filename}") for notebook in workflow.notebooks) | ||
|
|
||
|
|
||
| def test_dominated_init_is_pruned_with_its_notebook() -> None: |
There was a problem hiding this comment.
Nit: this checks the notebook is dropped, but not the init's setup tasks. A test where a real ForEach inner-job parameter keeps an init alive would help too.
Port of databricks-field-eng/flowx#17 onto the restructured codebase (
translator/engine.pyis nowsources/adf/translate.py+ir_serde.py). Supersedes #40.# Databricks notebook sourcemarker sobundle validateaccepts the notebook_task.mainby therender_bridge_notebookconsolidation. No source change here — this PR only adds a guard test.run_if: NONE_FAILEDso a failure in the taken branch propagates (AT_LEAST_ONE_SUCCESSlet a sibling success mask it)._init_<var>tasks that nothing references are pruned.Changes
Where pruning runs. #17 pruned during IR serialisation. Here it runs at the end of
prepare_workflow, which both packaging paths share. Only at that point are all references final:{{tasks._init_X.values.X}};source_task_key, ForEach inner-job parameters, andtaskValues.get(taskKey=...)reads in generated notebooks.Pruning on the IR would drop an init that an AppendVariable still needs.
How the pruned tasks are identified.
Pipeline.synthesized_variable_init_keys.translation_report.jsononly when non-empty, so Airflow reports are unchanged.pipeline_dict_to_ir.What counts as a reference. Only these structured forms count:
tasks.<key>.valuestoken;depends_onedge;taskKey='<key>'read in notebook code.So a sibling key like
_init_x_2, or a path like/Shared/_init_x_report, never keeps a dead_init_xalive. An init read by another init stays live. A pruned init also drops its notebook and setup tasks.A dead init still appears in
translation_report.json; it is removed only from the generated bundle.Tests
manually tested
added unit tests
added integration tests
tests/unit/test_control_flow_fixes.pycovers:Results:
pytest tests/unit: 1470 passed. The 2 failures intest_airflow_provider_sync.pyare environmental (gpg signing in the fixture'sgit commit) and also fail onmain.pytest tests/integration -m "not slow and not integration": 141 passed.ruffandmypyare clean.Before/after comparison: I packaged every ADF fixture in
tests/resources/jsononmainand on this branch._init_tasks and their notebooks are gone. A substring search ofmain's packaged output finds no other mention of any of them, so they were dead.AT_LEAST_ONE_SUCCESStoNONE_FAILED.This pull request and its description were written by Isaac.