Decouple Serialization and Deserialization Code for tasks - #54569

Merged
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults
Aug 29, 2025
Merged

Decouple Serialization and Deserialization Code for tasks#54569
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults

Conversation

@kaxil

@kaxilkaxil commented Aug 16, 2025

Copy link
Copy Markdown
Member

🎯 Problem Statement

The Task SDK separation in Airflow 3.1 requires decoupling serialization and deserialization code to eliminate server-side dependencies on client SDK implementations:

  1. Task SDK Dependencies: airflow-core deserialization currently depends on Task SDK's BaseOperator for default values and field lists
  2. Architectural Coupling: Server components import and use Task SDK classes during deserialization, violating client/server separation
  3. Independent Deployment Blocker: Tight coupling prevents independent deployment and upgrade of server vs client components

🚀 Solution Overview

This PR decouples (to a great extent) serialization and deserialization code by removing Task SDK dependencies from airflow-core:

  • Remove dynamic SDK calls: Replace get_serialized_fields() calls with hardcoded class methods
  • Eliminate import dependencies: Remove OPERATOR_DEFAULTS and other Task SDK imports from server-side code
  • Schema-driven defaults: Use schema.json and client_defaults instead of Task SDK classes for default resolution
  • Independent deployment: Enable server/client components to be deployed and upgraded separately

📊 Benchmark

As part of this change, I optimised how the defaults are stored and when a field is stored and removed anything that matches defaults, nulls and the bigger impact change to remove storing entire callback functions as strings and instead store a boolean to indicate if a callback was set or not.

The bigger the DAG (more tasks + especially with callbacks), the more savings.

Using actual pre-optimization code:

ScenarioTasksBeforeAfterSavedReduction
Basic DAGs107.5 KB5.7 KB1.8 KB24.0%
5033.7 KB24.5 KB9.2 KB27.3%
10066.4 KB48.0 KB18.4 KB27.7%
Production DAGs (3 callbacks/task)1015.5 KB6.6 KB8.9 KB57.4%
5073.8 KB28.9 KB44.9 KB60.8%
100146.7 KB56.9 KB89.8 KB61.2%

🔥 Callback Optimization Analysis (100 tasks with 3 callbacks each):

Storage MethodBeforeAfterSavedReduction
Callback representationLists of function codeBoolean flags--
Per-task callback overhead822 bytes91 bytes731 bytes89%
Total callback overhead80.3 KB8.9 KB71.4 KB89%
Per-task total size1,502 bytes583 bytes919 bytes61%

🎯 Key Optimization Impact:

  • Callback transformation: 89% reduction in callback storage overhead (function code → boolean flags)
  • Production scaling: 57% → 61% reduction as DAG size increases (10 → 100 tasks)
  • Per-task efficiency: 919 bytes saved per task (1,502 → 583 bytes) for callback DAGs
  • Consistent baseline: 24-28% reduction even for basic DAGs without callbacks

🏗️ Architecture Changes

Task Default Resolution

Implements hierarchical defaults during deserialization:

  1. Schema defaults (from schema.json) - lowest priority
  2. client_defaults.tasks - SDK-specific overrides
  3. partial_kwargs - MappedOperator values
  4. Explicit task values - highest priority

Serialization Exclusion

Fields matching client_defaults are automatically excluded from task serialization, reducing redundancy while maintaining full information.

fyi: Following the Task Execution API pattern, I aim to add versioned schema contract at Airflow website directly or version docs soon'ish:

Thinking about a URL like: https://airflow.apache.org/schemas/dag-serialization/v2.json

🚦 Migration Path

For Users

  • No action required - changes are completely transparent
  • Existing DAGs continue working unchanged
  • New DAGs automatically benefit from optimizations

Appendix (for my own tracking)

TODOs (some might be done in a future PR):

  • Add defaults to schema.json
  • Exclude defaults in schema from Serialized JSON
  • Change on_*_callback on tasks to use has_on_*_callback
  • Remove unmap method from scheduler-side #54816
  • Implement client_defaults generation in serialization (Task SDK side)
  • Verify if the change is backwards compatible. If not, Update serialization version to v3 and add backwards compatibility for v2. Update: It is a backwards-compatible change
  • Add tests to ensure defaults in Schema are same as the Server side classes. Or better add prek/pre-commit to autogenerate default from server-side to Schema
  • Evaluate the alternative of storing the list of attributes needed for Serialization & De-serialization in schema.JSON

Future Work:

  • Move the S10n code over for DAG & Task Group classes to Server-side
  • Include schema.json in the calver OpenAPI spec for Execution API and/or in airflow versioned docs
  • Move Serialization code to Task SDK
  • Remove ui_color & ui_fgcolor

Other points

  • If we move serialization to Task SDK and keep de-serializtion to the Server side, how do we handle the following:
    • XCom deserialization -- it currently uses the airflow.serialization module
    • ExtendedJSON - TypeDecorator used in serialization of the following:
      • DagRun.context_carrier
      • TaskInstance.next_kwargs

Benchmark script:

#!/usr/bin/env python3"""Script to measure real serialization scaling for different DAG sizes.Uses actual JSON examination for accurate before/after comparison."""importjsonimportsysfromdatetimeimportdatetime, timedeltafrompathlibimportPathfromairflow.sdkimportDAGfromairflow.providers.standard.operators.bashimportBashOperatorfromairflow.serialization.serialized_objectsimportSerializedDAGdefsuccess_callback(context):
"""Example success callback function."""print(f"Task {context['task_instance'].task_id} succeeded!")
return"success"deffailure_callback(context):
"""Example failure callback function."""print(f"Task {context['task_instance'].task_id} failed!")
# Send notification to Slackimportrequestsrequests.post("https://hooks.slack.com/webhook", json={
"text": f"❌ Task failed: {context['task_instance'].task_id}"
})
return"failure_handled"defretry_callback(context):
"""Example retry callback function."""print(f"Task {context['task_instance'].task_id} will retry!")
return"retry_scheduled"defcreate_test_dag(num_tasks: int, with_callbacks: bool=True) ->DAG:
"""Create a test DAG with specified number of tasks."""dag=DAG(
dag_id=f"scaling_test_dag_{num_tasks}",
start_date=datetime(2024, 1, 1),
schedule="@daily",
default_args={
"owner": "test_user",
"retries": 2,
"retry_delay": timedelta(minutes=5),
"email": "test@example.com",
"email_on_failure": True,
"email_on_retry": True,
}
)
withdag:
foriinrange(num_tasks):
task_kwargs= {
"task_id": f"task_{i:03d}",
"bash_command": f"echo 'Processing item {i}'",
"email_on_failure": True,
"email_on_retry": True,
}
ifwith_callbacks:
task_kwargs.update({
"on_success_callback": success_callback,
"on_failure_callback": failure_callback,
"on_retry_callback": retry_callback,
})
BashOperator(**task_kwargs)
returndagdefmeasure_dag_serialization(num_tasks: int, with_callbacks: bool=True) ->dict:
"""Measure serialization for a DAG with specified parameters."""dag=create_test_dag(num_tasks, with_callbacks)
try:
serialized=SerializedDAG.to_dict(dag)
exceptExceptionase:
return {"error": str(e)}
# Convert to compact JSONjson_compact=json.dumps(serialized, separators=(',', ':'))
total_size=len(json_compact.encode('utf-8'))
# Analyze callback fields if callbacks are enabledcallback_info= {}
ifwith_callbacksand"dag"inserializedand"tasks"inserialized["dag"]:
tasks=serialized["dag"]["tasks"]
iftasks:
first_task=tasks[0]["__var"]
# Find callback fieldscallback_fields= [kforkinfirst_taskif"callback"ink.lower()]
# Calculate callback overheadtask_json=json.dumps(first_task, separators=(',', ':'))
total_task_size=len(task_json.encode('utf-8'))
task_without_callbacks= {k: vfork, vinfirst_task.items() if"callback"notink.lower()}
no_callback_json=json.dumps(task_without_callbacks, separators=(',', ':'))
no_callback_size=len(no_callback_json.encode('utf-8'))
callback_overhead_per_task=total_task_size-no_callback_sizetotal_callback_overhead=callback_overhead_per_task*len(tasks)
callback_info= {
"callback_fields": callback_fields,
"callback_overhead_per_task": callback_overhead_per_task,
"total_callback_overhead": total_callback_overhead,
"task_size": total_task_size,
"task_size_without_callbacks": no_callback_size
}
return {
"num_tasks": num_tasks,
"with_callbacks": with_callbacks,
"total_size": total_size,
"size_kb": total_size/1024,
"per_task_bytes": total_size/num_tasks,
"callback_info": callback_info
}
defrun_scaling_measurements():
"""Run measurements for different DAG sizes."""print("🔍 Measuring Real Serialization Scaling")
print("="*60)
task_counts= [10, 25, 50, 100]
# Test with callbacksprint("\n📊 **Production DAGs (With Callbacks)**")
print("| Tasks | Size | Per Task | Callback Overhead |")
print("|-------|------|----------|-------------------|")
callback_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=True)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuecallback_results.append(result)
callback_overhead_kb=result["callback_info"]["total_callback_overhead"] /1024ifresult["callback_info"] else0print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes | {callback_overhead_kb:.1f} KB |")
# Test without callbacksprint("\n📊 **Basic DAGs (No Callbacks)**")
print("| Tasks | Size | Per Task |")
print("|-------|------|----------|")
basic_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=False)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuebasic_results.append(result)
print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes |")
# Show callback field details for largest DAGifcallback_results:
largest=callback_results[-1]
iflargest["callback_info"]:
print(f"\n🔍 **Callback Analysis (100 tasks):**")
ci=largest["callback_info"]
print(f" Callback fields: {ci['callback_fields']}")
print(f" Per-task callback overhead: {ci['callback_overhead_per_task']} bytes")
print(f" Total callback overhead: {ci['total_callback_overhead']:,} bytes ({ci['total_callback_overhead']/1024:.1f} KB)")
print(f" Task size with callbacks: {ci['task_size']} bytes")
print(f" Task size without callbacks: {ci['task_size_without_callbacks']} bytes")
# Generate CSV for easy importprint(f"\n📋 **CSV Format (for PR description):**")
print("# callbacks")
forresultincallback_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},True")
print("# basic") forresultinbasic_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},False")
returncallback_results, basic_resultsdefmain():
"""Main function."""run_scaling_measurements()
if__name__=="__main__":
main()

Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 8baf823 to 0334138CompareAugust 20, 2025 14:58
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 8248e4a to c126079CompareAugust 23, 2025 22:52
@kaxilkaxil mentioned this pull request Aug 26, 2025
4 tasks
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 89a6807 to c0be635CompareAugust 27, 2025 07:29
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 6351823 to 3674170CompareAugust 28, 2025 19:42
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 7850b64 to 93c2642CompareAugust 28, 2025 22:58
@kaxilkaxil changed the title [DO NOT REVIEW] Remove Task SDK dependencies from airflow-core deserializationDecouple Serialization and Deserialization Code for OperatorsAug 28, 2025
@kaxilkaxil changed the title Decouple Serialization and Deserialization Code for OperatorsDecouple Serialization and Deserialization Code for tasksAug 28, 2025
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 93c2642 to 3bfc590CompareAugust 29, 2025 00:39
@kaxilkaxil added the full tests needed We need to run full set of tests for this PR to merge label Aug 29, 2025
@kaxil
kaxil marked this pull request as ready for review August 29, 2025 01:25
Comment threadairflow-core/docs/administration-and-deployment/dag-serialization.rst Outdated
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py Outdated
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 3bfc590 to eb97006CompareAugust 29, 2025 22:42
@kaxil
kaxil merged commit d9969be into apache:mainAug 29, 2025
107 checks passed
@kaxil
kaxil deleted the serialization/op-defaults branch August 29, 2025 23:29
mangal-vairalkar pushed a commit to mangal-vairalkar/airflow that referenced this pull request Aug 30, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
bggwak pushed a commit to bggwak/airflow that referenced this pull request Sep 2, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
(cherry picked from commit a582464)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:serializationarea:task-sdkfull tests neededWe need to run full set of tests for this PR to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@kaxil@jedcunningham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Decouple Serialization and Deserialization Code for tasks - #54569

Merged
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults
Aug 29, 2025
Merged

Decouple Serialization and Deserialization Code for tasks#54569
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults

Conversation

@kaxil

@kaxilkaxil commented Aug 16, 2025

Copy link
Copy Markdown
Member

🎯 Problem Statement

The Task SDK separation in Airflow 3.1 requires decoupling serialization and deserialization code to eliminate server-side dependencies on client SDK implementations:

  1. Task SDK Dependencies: airflow-core deserialization currently depends on Task SDK's BaseOperator for default values and field lists
  2. Architectural Coupling: Server components import and use Task SDK classes during deserialization, violating client/server separation
  3. Independent Deployment Blocker: Tight coupling prevents independent deployment and upgrade of server vs client components

🚀 Solution Overview

This PR decouples (to a great extent) serialization and deserialization code by removing Task SDK dependencies from airflow-core:

  • Remove dynamic SDK calls: Replace get_serialized_fields() calls with hardcoded class methods
  • Eliminate import dependencies: Remove OPERATOR_DEFAULTS and other Task SDK imports from server-side code
  • Schema-driven defaults: Use schema.json and client_defaults instead of Task SDK classes for default resolution
  • Independent deployment: Enable server/client components to be deployed and upgraded separately

📊 Benchmark

As part of this change, I optimised how the defaults are stored and when a field is stored and removed anything that matches defaults, nulls and the bigger impact change to remove storing entire callback functions as strings and instead store a boolean to indicate if a callback was set or not.

The bigger the DAG (more tasks + especially with callbacks), the more savings.

Using actual pre-optimization code:

ScenarioTasksBeforeAfterSavedReduction
Basic DAGs107.5 KB5.7 KB1.8 KB24.0%
5033.7 KB24.5 KB9.2 KB27.3%
10066.4 KB48.0 KB18.4 KB27.7%
Production DAGs (3 callbacks/task)1015.5 KB6.6 KB8.9 KB57.4%
5073.8 KB28.9 KB44.9 KB60.8%
100146.7 KB56.9 KB89.8 KB61.2%

🔥 Callback Optimization Analysis (100 tasks with 3 callbacks each):

Storage MethodBeforeAfterSavedReduction
Callback representationLists of function codeBoolean flags--
Per-task callback overhead822 bytes91 bytes731 bytes89%
Total callback overhead80.3 KB8.9 KB71.4 KB89%
Per-task total size1,502 bytes583 bytes919 bytes61%

🎯 Key Optimization Impact:

  • Callback transformation: 89% reduction in callback storage overhead (function code → boolean flags)
  • Production scaling: 57% → 61% reduction as DAG size increases (10 → 100 tasks)
  • Per-task efficiency: 919 bytes saved per task (1,502 → 583 bytes) for callback DAGs
  • Consistent baseline: 24-28% reduction even for basic DAGs without callbacks

🏗️ Architecture Changes

Task Default Resolution

Implements hierarchical defaults during deserialization:

  1. Schema defaults (from schema.json) - lowest priority
  2. client_defaults.tasks - SDK-specific overrides
  3. partial_kwargs - MappedOperator values
  4. Explicit task values - highest priority

Serialization Exclusion

Fields matching client_defaults are automatically excluded from task serialization, reducing redundancy while maintaining full information.

fyi: Following the Task Execution API pattern, I aim to add versioned schema contract at Airflow website directly or version docs soon'ish:

Thinking about a URL like: https://airflow.apache.org/schemas/dag-serialization/v2.json

🚦 Migration Path

For Users

  • No action required - changes are completely transparent
  • Existing DAGs continue working unchanged
  • New DAGs automatically benefit from optimizations

Appendix (for my own tracking)

TODOs (some might be done in a future PR):

  • Add defaults to schema.json
  • Exclude defaults in schema from Serialized JSON
  • Change on_*_callback on tasks to use has_on_*_callback
  • Remove unmap method from scheduler-side #54816
  • Implement client_defaults generation in serialization (Task SDK side)
  • Verify if the change is backwards compatible. If not, Update serialization version to v3 and add backwards compatibility for v2. Update: It is a backwards-compatible change
  • Add tests to ensure defaults in Schema are same as the Server side classes. Or better add prek/pre-commit to autogenerate default from server-side to Schema
  • Evaluate the alternative of storing the list of attributes needed for Serialization & De-serialization in schema.JSON

Future Work:

  • Move the S10n code over for DAG & Task Group classes to Server-side
  • Include schema.json in the calver OpenAPI spec for Execution API and/or in airflow versioned docs
  • Move Serialization code to Task SDK
  • Remove ui_color & ui_fgcolor

Other points

  • If we move serialization to Task SDK and keep de-serializtion to the Server side, how do we handle the following:
    • XCom deserialization -- it currently uses the airflow.serialization module
    • ExtendedJSON - TypeDecorator used in serialization of the following:
      • DagRun.context_carrier
      • TaskInstance.next_kwargs

Benchmark script:

#!/usr/bin/env python3"""Script to measure real serialization scaling for different DAG sizes.Uses actual JSON examination for accurate before/after comparison."""importjsonimportsysfromdatetimeimportdatetime, timedeltafrompathlibimportPathfromairflow.sdkimportDAGfromairflow.providers.standard.operators.bashimportBashOperatorfromairflow.serialization.serialized_objectsimportSerializedDAGdefsuccess_callback(context):
"""Example success callback function."""print(f"Task {context['task_instance'].task_id} succeeded!")
return"success"deffailure_callback(context):
"""Example failure callback function."""print(f"Task {context['task_instance'].task_id} failed!")
# Send notification to Slackimportrequestsrequests.post("https://hooks.slack.com/webhook", json={
"text": f"❌ Task failed: {context['task_instance'].task_id}"
})
return"failure_handled"defretry_callback(context):
"""Example retry callback function."""print(f"Task {context['task_instance'].task_id} will retry!")
return"retry_scheduled"defcreate_test_dag(num_tasks: int, with_callbacks: bool=True) ->DAG:
"""Create a test DAG with specified number of tasks."""dag=DAG(
dag_id=f"scaling_test_dag_{num_tasks}",
start_date=datetime(2024, 1, 1),
schedule="@daily",
default_args={
"owner": "test_user",
"retries": 2,
"retry_delay": timedelta(minutes=5),
"email": "test@example.com",
"email_on_failure": True,
"email_on_retry": True,
}
)
withdag:
foriinrange(num_tasks):
task_kwargs= {
"task_id": f"task_{i:03d}",
"bash_command": f"echo 'Processing item {i}'",
"email_on_failure": True,
"email_on_retry": True,
}
ifwith_callbacks:
task_kwargs.update({
"on_success_callback": success_callback,
"on_failure_callback": failure_callback,
"on_retry_callback": retry_callback,
})
BashOperator(**task_kwargs)
returndagdefmeasure_dag_serialization(num_tasks: int, with_callbacks: bool=True) ->dict:
"""Measure serialization for a DAG with specified parameters."""dag=create_test_dag(num_tasks, with_callbacks)
try:
serialized=SerializedDAG.to_dict(dag)
exceptExceptionase:
return {"error": str(e)}
# Convert to compact JSONjson_compact=json.dumps(serialized, separators=(',', ':'))
total_size=len(json_compact.encode('utf-8'))
# Analyze callback fields if callbacks are enabledcallback_info= {}
ifwith_callbacksand"dag"inserializedand"tasks"inserialized["dag"]:
tasks=serialized["dag"]["tasks"]
iftasks:
first_task=tasks[0]["__var"]
# Find callback fieldscallback_fields= [kforkinfirst_taskif"callback"ink.lower()]
# Calculate callback overheadtask_json=json.dumps(first_task, separators=(',', ':'))
total_task_size=len(task_json.encode('utf-8'))
task_without_callbacks= {k: vfork, vinfirst_task.items() if"callback"notink.lower()}
no_callback_json=json.dumps(task_without_callbacks, separators=(',', ':'))
no_callback_size=len(no_callback_json.encode('utf-8'))
callback_overhead_per_task=total_task_size-no_callback_sizetotal_callback_overhead=callback_overhead_per_task*len(tasks)
callback_info= {
"callback_fields": callback_fields,
"callback_overhead_per_task": callback_overhead_per_task,
"total_callback_overhead": total_callback_overhead,
"task_size": total_task_size,
"task_size_without_callbacks": no_callback_size
}
return {
"num_tasks": num_tasks,
"with_callbacks": with_callbacks,
"total_size": total_size,
"size_kb": total_size/1024,
"per_task_bytes": total_size/num_tasks,
"callback_info": callback_info
}
defrun_scaling_measurements():
"""Run measurements for different DAG sizes."""print("🔍 Measuring Real Serialization Scaling")
print("="*60)
task_counts= [10, 25, 50, 100]
# Test with callbacksprint("\n📊 **Production DAGs (With Callbacks)**")
print("| Tasks | Size | Per Task | Callback Overhead |")
print("|-------|------|----------|-------------------|")
callback_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=True)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuecallback_results.append(result)
callback_overhead_kb=result["callback_info"]["total_callback_overhead"] /1024ifresult["callback_info"] else0print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes | {callback_overhead_kb:.1f} KB |")
# Test without callbacksprint("\n📊 **Basic DAGs (No Callbacks)**")
print("| Tasks | Size | Per Task |")
print("|-------|------|----------|")
basic_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=False)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuebasic_results.append(result)
print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes |")
# Show callback field details for largest DAGifcallback_results:
largest=callback_results[-1]
iflargest["callback_info"]:
print(f"\n🔍 **Callback Analysis (100 tasks):**")
ci=largest["callback_info"]
print(f" Callback fields: {ci['callback_fields']}")
print(f" Per-task callback overhead: {ci['callback_overhead_per_task']} bytes")
print(f" Total callback overhead: {ci['total_callback_overhead']:,} bytes ({ci['total_callback_overhead']/1024:.1f} KB)")
print(f" Task size with callbacks: {ci['task_size']} bytes")
print(f" Task size without callbacks: {ci['task_size_without_callbacks']} bytes")
# Generate CSV for easy importprint(f"\n📋 **CSV Format (for PR description):**")
print("# callbacks")
forresultincallback_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},True")
print("# basic") forresultinbasic_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},False")
returncallback_results, basic_resultsdefmain():
"""Main function."""run_scaling_measurements()
if__name__=="__main__":
main()

Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 8baf823 to 0334138CompareAugust 20, 2025 14:58
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 8248e4a to c126079CompareAugust 23, 2025 22:52
@kaxilkaxil mentioned this pull request Aug 26, 2025
4 tasks
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 89a6807 to c0be635CompareAugust 27, 2025 07:29
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 6351823 to 3674170CompareAugust 28, 2025 19:42
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 7850b64 to 93c2642CompareAugust 28, 2025 22:58
@kaxilkaxil changed the title [DO NOT REVIEW] Remove Task SDK dependencies from airflow-core deserializationDecouple Serialization and Deserialization Code for OperatorsAug 28, 2025
@kaxilkaxil changed the title Decouple Serialization and Deserialization Code for OperatorsDecouple Serialization and Deserialization Code for tasksAug 28, 2025
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 93c2642 to 3bfc590CompareAugust 29, 2025 00:39
@kaxilkaxil added the full tests needed We need to run full set of tests for this PR to merge label Aug 29, 2025
@kaxil
kaxil marked this pull request as ready for review August 29, 2025 01:25
Comment threadairflow-core/docs/administration-and-deployment/dag-serialization.rst Outdated
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py Outdated
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 3bfc590 to eb97006CompareAugust 29, 2025 22:42
@kaxil
kaxil merged commit d9969be into apache:mainAug 29, 2025
107 checks passed
@kaxil
kaxil deleted the serialization/op-defaults branch August 29, 2025 23:29
mangal-vairalkar pushed a commit to mangal-vairalkar/airflow that referenced this pull request Aug 30, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
bggwak pushed a commit to bggwak/airflow that referenced this pull request Sep 2, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
(cherry picked from commit a582464)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:serializationarea:task-sdkfull tests neededWe need to run full set of tests for this PR to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@kaxil@jedcunningham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Decouple Serialization and Deserialization Code for tasks - #54569

Merged
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults
Aug 29, 2025
Merged

Decouple Serialization and Deserialization Code for tasks#54569
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults

Conversation

@kaxil

@kaxilkaxil commented Aug 16, 2025

Copy link
Copy Markdown
Member

🎯 Problem Statement

The Task SDK separation in Airflow 3.1 requires decoupling serialization and deserialization code to eliminate server-side dependencies on client SDK implementations:

  1. Task SDK Dependencies: airflow-core deserialization currently depends on Task SDK's BaseOperator for default values and field lists
  2. Architectural Coupling: Server components import and use Task SDK classes during deserialization, violating client/server separation
  3. Independent Deployment Blocker: Tight coupling prevents independent deployment and upgrade of server vs client components

🚀 Solution Overview

This PR decouples (to a great extent) serialization and deserialization code by removing Task SDK dependencies from airflow-core:

  • Remove dynamic SDK calls: Replace get_serialized_fields() calls with hardcoded class methods
  • Eliminate import dependencies: Remove OPERATOR_DEFAULTS and other Task SDK imports from server-side code
  • Schema-driven defaults: Use schema.json and client_defaults instead of Task SDK classes for default resolution
  • Independent deployment: Enable server/client components to be deployed and upgraded separately

📊 Benchmark

As part of this change, I optimised how the defaults are stored and when a field is stored and removed anything that matches defaults, nulls and the bigger impact change to remove storing entire callback functions as strings and instead store a boolean to indicate if a callback was set or not.

The bigger the DAG (more tasks + especially with callbacks), the more savings.

Using actual pre-optimization code:

ScenarioTasksBeforeAfterSavedReduction
Basic DAGs107.5 KB5.7 KB1.8 KB24.0%
5033.7 KB24.5 KB9.2 KB27.3%
10066.4 KB48.0 KB18.4 KB27.7%
Production DAGs (3 callbacks/task)1015.5 KB6.6 KB8.9 KB57.4%
5073.8 KB28.9 KB44.9 KB60.8%
100146.7 KB56.9 KB89.8 KB61.2%

🔥 Callback Optimization Analysis (100 tasks with 3 callbacks each):

Storage MethodBeforeAfterSavedReduction
Callback representationLists of function codeBoolean flags--
Per-task callback overhead822 bytes91 bytes731 bytes89%
Total callback overhead80.3 KB8.9 KB71.4 KB89%
Per-task total size1,502 bytes583 bytes919 bytes61%

🎯 Key Optimization Impact:

  • Callback transformation: 89% reduction in callback storage overhead (function code → boolean flags)
  • Production scaling: 57% → 61% reduction as DAG size increases (10 → 100 tasks)
  • Per-task efficiency: 919 bytes saved per task (1,502 → 583 bytes) for callback DAGs
  • Consistent baseline: 24-28% reduction even for basic DAGs without callbacks

🏗️ Architecture Changes

Task Default Resolution

Implements hierarchical defaults during deserialization:

  1. Schema defaults (from schema.json) - lowest priority
  2. client_defaults.tasks - SDK-specific overrides
  3. partial_kwargs - MappedOperator values
  4. Explicit task values - highest priority

Serialization Exclusion

Fields matching client_defaults are automatically excluded from task serialization, reducing redundancy while maintaining full information.

fyi: Following the Task Execution API pattern, I aim to add versioned schema contract at Airflow website directly or version docs soon'ish:

Thinking about a URL like: https://airflow.apache.org/schemas/dag-serialization/v2.json

🚦 Migration Path

For Users

  • No action required - changes are completely transparent
  • Existing DAGs continue working unchanged
  • New DAGs automatically benefit from optimizations

Appendix (for my own tracking)

TODOs (some might be done in a future PR):

  • Add defaults to schema.json
  • Exclude defaults in schema from Serialized JSON
  • Change on_*_callback on tasks to use has_on_*_callback
  • Remove unmap method from scheduler-side #54816
  • Implement client_defaults generation in serialization (Task SDK side)
  • Verify if the change is backwards compatible. If not, Update serialization version to v3 and add backwards compatibility for v2. Update: It is a backwards-compatible change
  • Add tests to ensure defaults in Schema are same as the Server side classes. Or better add prek/pre-commit to autogenerate default from server-side to Schema
  • Evaluate the alternative of storing the list of attributes needed for Serialization & De-serialization in schema.JSON

Future Work:

  • Move the S10n code over for DAG & Task Group classes to Server-side
  • Include schema.json in the calver OpenAPI spec for Execution API and/or in airflow versioned docs
  • Move Serialization code to Task SDK
  • Remove ui_color & ui_fgcolor

Other points

  • If we move serialization to Task SDK and keep de-serializtion to the Server side, how do we handle the following:
    • XCom deserialization -- it currently uses the airflow.serialization module
    • ExtendedJSON - TypeDecorator used in serialization of the following:
      • DagRun.context_carrier
      • TaskInstance.next_kwargs

Benchmark script:

#!/usr/bin/env python3"""Script to measure real serialization scaling for different DAG sizes.Uses actual JSON examination for accurate before/after comparison."""importjsonimportsysfromdatetimeimportdatetime, timedeltafrompathlibimportPathfromairflow.sdkimportDAGfromairflow.providers.standard.operators.bashimportBashOperatorfromairflow.serialization.serialized_objectsimportSerializedDAGdefsuccess_callback(context):
"""Example success callback function."""print(f"Task {context['task_instance'].task_id} succeeded!")
return"success"deffailure_callback(context):
"""Example failure callback function."""print(f"Task {context['task_instance'].task_id} failed!")
# Send notification to Slackimportrequestsrequests.post("https://hooks.slack.com/webhook", json={
"text": f"❌ Task failed: {context['task_instance'].task_id}"
})
return"failure_handled"defretry_callback(context):
"""Example retry callback function."""print(f"Task {context['task_instance'].task_id} will retry!")
return"retry_scheduled"defcreate_test_dag(num_tasks: int, with_callbacks: bool=True) ->DAG:
"""Create a test DAG with specified number of tasks."""dag=DAG(
dag_id=f"scaling_test_dag_{num_tasks}",
start_date=datetime(2024, 1, 1),
schedule="@daily",
default_args={
"owner": "test_user",
"retries": 2,
"retry_delay": timedelta(minutes=5),
"email": "test@example.com",
"email_on_failure": True,
"email_on_retry": True,
}
)
withdag:
foriinrange(num_tasks):
task_kwargs= {
"task_id": f"task_{i:03d}",
"bash_command": f"echo 'Processing item {i}'",
"email_on_failure": True,
"email_on_retry": True,
}
ifwith_callbacks:
task_kwargs.update({
"on_success_callback": success_callback,
"on_failure_callback": failure_callback,
"on_retry_callback": retry_callback,
})
BashOperator(**task_kwargs)
returndagdefmeasure_dag_serialization(num_tasks: int, with_callbacks: bool=True) ->dict:
"""Measure serialization for a DAG with specified parameters."""dag=create_test_dag(num_tasks, with_callbacks)
try:
serialized=SerializedDAG.to_dict(dag)
exceptExceptionase:
return {"error": str(e)}
# Convert to compact JSONjson_compact=json.dumps(serialized, separators=(',', ':'))
total_size=len(json_compact.encode('utf-8'))
# Analyze callback fields if callbacks are enabledcallback_info= {}
ifwith_callbacksand"dag"inserializedand"tasks"inserialized["dag"]:
tasks=serialized["dag"]["tasks"]
iftasks:
first_task=tasks[0]["__var"]
# Find callback fieldscallback_fields= [kforkinfirst_taskif"callback"ink.lower()]
# Calculate callback overheadtask_json=json.dumps(first_task, separators=(',', ':'))
total_task_size=len(task_json.encode('utf-8'))
task_without_callbacks= {k: vfork, vinfirst_task.items() if"callback"notink.lower()}
no_callback_json=json.dumps(task_without_callbacks, separators=(',', ':'))
no_callback_size=len(no_callback_json.encode('utf-8'))
callback_overhead_per_task=total_task_size-no_callback_sizetotal_callback_overhead=callback_overhead_per_task*len(tasks)
callback_info= {
"callback_fields": callback_fields,
"callback_overhead_per_task": callback_overhead_per_task,
"total_callback_overhead": total_callback_overhead,
"task_size": total_task_size,
"task_size_without_callbacks": no_callback_size
}
return {
"num_tasks": num_tasks,
"with_callbacks": with_callbacks,
"total_size": total_size,
"size_kb": total_size/1024,
"per_task_bytes": total_size/num_tasks,
"callback_info": callback_info
}
defrun_scaling_measurements():
"""Run measurements for different DAG sizes."""print("🔍 Measuring Real Serialization Scaling")
print("="*60)
task_counts= [10, 25, 50, 100]
# Test with callbacksprint("\n📊 **Production DAGs (With Callbacks)**")
print("| Tasks | Size | Per Task | Callback Overhead |")
print("|-------|------|----------|-------------------|")
callback_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=True)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuecallback_results.append(result)
callback_overhead_kb=result["callback_info"]["total_callback_overhead"] /1024ifresult["callback_info"] else0print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes | {callback_overhead_kb:.1f} KB |")
# Test without callbacksprint("\n📊 **Basic DAGs (No Callbacks)**")
print("| Tasks | Size | Per Task |")
print("|-------|------|----------|")
basic_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=False)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuebasic_results.append(result)
print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes |")
# Show callback field details for largest DAGifcallback_results:
largest=callback_results[-1]
iflargest["callback_info"]:
print(f"\n🔍 **Callback Analysis (100 tasks):**")
ci=largest["callback_info"]
print(f" Callback fields: {ci['callback_fields']}")
print(f" Per-task callback overhead: {ci['callback_overhead_per_task']} bytes")
print(f" Total callback overhead: {ci['total_callback_overhead']:,} bytes ({ci['total_callback_overhead']/1024:.1f} KB)")
print(f" Task size with callbacks: {ci['task_size']} bytes")
print(f" Task size without callbacks: {ci['task_size_without_callbacks']} bytes")
# Generate CSV for easy importprint(f"\n📋 **CSV Format (for PR description):**")
print("# callbacks")
forresultincallback_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},True")
print("# basic") forresultinbasic_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},False")
returncallback_results, basic_resultsdefmain():
"""Main function."""run_scaling_measurements()
if__name__=="__main__":
main()

Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 8baf823 to 0334138CompareAugust 20, 2025 14:58
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 8248e4a to c126079CompareAugust 23, 2025 22:52
@kaxilkaxil mentioned this pull request Aug 26, 2025
4 tasks
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 89a6807 to c0be635CompareAugust 27, 2025 07:29
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 6351823 to 3674170CompareAugust 28, 2025 19:42
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 7850b64 to 93c2642CompareAugust 28, 2025 22:58
@kaxilkaxil changed the title [DO NOT REVIEW] Remove Task SDK dependencies from airflow-core deserializationDecouple Serialization and Deserialization Code for OperatorsAug 28, 2025
@kaxilkaxil changed the title Decouple Serialization and Deserialization Code for OperatorsDecouple Serialization and Deserialization Code for tasksAug 28, 2025
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 93c2642 to 3bfc590CompareAugust 29, 2025 00:39
@kaxilkaxil added the full tests needed We need to run full set of tests for this PR to merge label Aug 29, 2025
@kaxil
kaxil marked this pull request as ready for review August 29, 2025 01:25
Comment threadairflow-core/docs/administration-and-deployment/dag-serialization.rst Outdated
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py Outdated
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 3bfc590 to eb97006CompareAugust 29, 2025 22:42
@kaxil
kaxil merged commit d9969be into apache:mainAug 29, 2025
107 checks passed
@kaxil
kaxil deleted the serialization/op-defaults branch August 29, 2025 23:29
mangal-vairalkar pushed a commit to mangal-vairalkar/airflow that referenced this pull request Aug 30, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
bggwak pushed a commit to bggwak/airflow that referenced this pull request Sep 2, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
(cherry picked from commit a582464)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:serializationarea:task-sdkfull tests neededWe need to run full set of tests for this PR to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@kaxil@jedcunningham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Decouple Serialization and Deserialization Code for tasks - #54569

Merged
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults
Aug 29, 2025
Merged

Decouple Serialization and Deserialization Code for tasks#54569
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults

Conversation

@kaxil

@kaxilkaxil commented Aug 16, 2025

Copy link
Copy Markdown
Member

🎯 Problem Statement

The Task SDK separation in Airflow 3.1 requires decoupling serialization and deserialization code to eliminate server-side dependencies on client SDK implementations:

  1. Task SDK Dependencies: airflow-core deserialization currently depends on Task SDK's BaseOperator for default values and field lists
  2. Architectural Coupling: Server components import and use Task SDK classes during deserialization, violating client/server separation
  3. Independent Deployment Blocker: Tight coupling prevents independent deployment and upgrade of server vs client components

🚀 Solution Overview

This PR decouples (to a great extent) serialization and deserialization code by removing Task SDK dependencies from airflow-core:

  • Remove dynamic SDK calls: Replace get_serialized_fields() calls with hardcoded class methods
  • Eliminate import dependencies: Remove OPERATOR_DEFAULTS and other Task SDK imports from server-side code
  • Schema-driven defaults: Use schema.json and client_defaults instead of Task SDK classes for default resolution
  • Independent deployment: Enable server/client components to be deployed and upgraded separately

📊 Benchmark

As part of this change, I optimised how the defaults are stored and when a field is stored and removed anything that matches defaults, nulls and the bigger impact change to remove storing entire callback functions as strings and instead store a boolean to indicate if a callback was set or not.

The bigger the DAG (more tasks + especially with callbacks), the more savings.

Using actual pre-optimization code:

ScenarioTasksBeforeAfterSavedReduction
Basic DAGs107.5 KB5.7 KB1.8 KB24.0%
5033.7 KB24.5 KB9.2 KB27.3%
10066.4 KB48.0 KB18.4 KB27.7%
Production DAGs (3 callbacks/task)1015.5 KB6.6 KB8.9 KB57.4%
5073.8 KB28.9 KB44.9 KB60.8%
100146.7 KB56.9 KB89.8 KB61.2%

🔥 Callback Optimization Analysis (100 tasks with 3 callbacks each):

Storage MethodBeforeAfterSavedReduction
Callback representationLists of function codeBoolean flags--
Per-task callback overhead822 bytes91 bytes731 bytes89%
Total callback overhead80.3 KB8.9 KB71.4 KB89%
Per-task total size1,502 bytes583 bytes919 bytes61%

🎯 Key Optimization Impact:

  • Callback transformation: 89% reduction in callback storage overhead (function code → boolean flags)
  • Production scaling: 57% → 61% reduction as DAG size increases (10 → 100 tasks)
  • Per-task efficiency: 919 bytes saved per task (1,502 → 583 bytes) for callback DAGs
  • Consistent baseline: 24-28% reduction even for basic DAGs without callbacks

🏗️ Architecture Changes

Task Default Resolution

Implements hierarchical defaults during deserialization:

  1. Schema defaults (from schema.json) - lowest priority
  2. client_defaults.tasks - SDK-specific overrides
  3. partial_kwargs - MappedOperator values
  4. Explicit task values - highest priority

Serialization Exclusion

Fields matching client_defaults are automatically excluded from task serialization, reducing redundancy while maintaining full information.

fyi: Following the Task Execution API pattern, I aim to add versioned schema contract at Airflow website directly or version docs soon'ish:

Thinking about a URL like: https://airflow.apache.org/schemas/dag-serialization/v2.json

🚦 Migration Path

For Users

  • No action required - changes are completely transparent
  • Existing DAGs continue working unchanged
  • New DAGs automatically benefit from optimizations

Appendix (for my own tracking)

TODOs (some might be done in a future PR):

  • Add defaults to schema.json
  • Exclude defaults in schema from Serialized JSON
  • Change on_*_callback on tasks to use has_on_*_callback
  • Remove unmap method from scheduler-side #54816
  • Implement client_defaults generation in serialization (Task SDK side)
  • Verify if the change is backwards compatible. If not, Update serialization version to v3 and add backwards compatibility for v2. Update: It is a backwards-compatible change
  • Add tests to ensure defaults in Schema are same as the Server side classes. Or better add prek/pre-commit to autogenerate default from server-side to Schema
  • Evaluate the alternative of storing the list of attributes needed for Serialization & De-serialization in schema.JSON

Future Work:

  • Move the S10n code over for DAG & Task Group classes to Server-side
  • Include schema.json in the calver OpenAPI spec for Execution API and/or in airflow versioned docs
  • Move Serialization code to Task SDK
  • Remove ui_color & ui_fgcolor

Other points

  • If we move serialization to Task SDK and keep de-serializtion to the Server side, how do we handle the following:
    • XCom deserialization -- it currently uses the airflow.serialization module
    • ExtendedJSON - TypeDecorator used in serialization of the following:
      • DagRun.context_carrier
      • TaskInstance.next_kwargs

Benchmark script:

#!/usr/bin/env python3"""Script to measure real serialization scaling for different DAG sizes.Uses actual JSON examination for accurate before/after comparison."""importjsonimportsysfromdatetimeimportdatetime, timedeltafrompathlibimportPathfromairflow.sdkimportDAGfromairflow.providers.standard.operators.bashimportBashOperatorfromairflow.serialization.serialized_objectsimportSerializedDAGdefsuccess_callback(context):
"""Example success callback function."""print(f"Task {context['task_instance'].task_id} succeeded!")
return"success"deffailure_callback(context):
"""Example failure callback function."""print(f"Task {context['task_instance'].task_id} failed!")
# Send notification to Slackimportrequestsrequests.post("https://hooks.slack.com/webhook", json={
"text": f"❌ Task failed: {context['task_instance'].task_id}"
})
return"failure_handled"defretry_callback(context):
"""Example retry callback function."""print(f"Task {context['task_instance'].task_id} will retry!")
return"retry_scheduled"defcreate_test_dag(num_tasks: int, with_callbacks: bool=True) ->DAG:
"""Create a test DAG with specified number of tasks."""dag=DAG(
dag_id=f"scaling_test_dag_{num_tasks}",
start_date=datetime(2024, 1, 1),
schedule="@daily",
default_args={
"owner": "test_user",
"retries": 2,
"retry_delay": timedelta(minutes=5),
"email": "test@example.com",
"email_on_failure": True,
"email_on_retry": True,
}
)
withdag:
foriinrange(num_tasks):
task_kwargs= {
"task_id": f"task_{i:03d}",
"bash_command": f"echo 'Processing item {i}'",
"email_on_failure": True,
"email_on_retry": True,
}
ifwith_callbacks:
task_kwargs.update({
"on_success_callback": success_callback,
"on_failure_callback": failure_callback,
"on_retry_callback": retry_callback,
})
BashOperator(**task_kwargs)
returndagdefmeasure_dag_serialization(num_tasks: int, with_callbacks: bool=True) ->dict:
"""Measure serialization for a DAG with specified parameters."""dag=create_test_dag(num_tasks, with_callbacks)
try:
serialized=SerializedDAG.to_dict(dag)
exceptExceptionase:
return {"error": str(e)}
# Convert to compact JSONjson_compact=json.dumps(serialized, separators=(',', ':'))
total_size=len(json_compact.encode('utf-8'))
# Analyze callback fields if callbacks are enabledcallback_info= {}
ifwith_callbacksand"dag"inserializedand"tasks"inserialized["dag"]:
tasks=serialized["dag"]["tasks"]
iftasks:
first_task=tasks[0]["__var"]
# Find callback fieldscallback_fields= [kforkinfirst_taskif"callback"ink.lower()]
# Calculate callback overheadtask_json=json.dumps(first_task, separators=(',', ':'))
total_task_size=len(task_json.encode('utf-8'))
task_without_callbacks= {k: vfork, vinfirst_task.items() if"callback"notink.lower()}
no_callback_json=json.dumps(task_without_callbacks, separators=(',', ':'))
no_callback_size=len(no_callback_json.encode('utf-8'))
callback_overhead_per_task=total_task_size-no_callback_sizetotal_callback_overhead=callback_overhead_per_task*len(tasks)
callback_info= {
"callback_fields": callback_fields,
"callback_overhead_per_task": callback_overhead_per_task,
"total_callback_overhead": total_callback_overhead,
"task_size": total_task_size,
"task_size_without_callbacks": no_callback_size
}
return {
"num_tasks": num_tasks,
"with_callbacks": with_callbacks,
"total_size": total_size,
"size_kb": total_size/1024,
"per_task_bytes": total_size/num_tasks,
"callback_info": callback_info
}
defrun_scaling_measurements():
"""Run measurements for different DAG sizes."""print("🔍 Measuring Real Serialization Scaling")
print("="*60)
task_counts= [10, 25, 50, 100]
# Test with callbacksprint("\n📊 **Production DAGs (With Callbacks)**")
print("| Tasks | Size | Per Task | Callback Overhead |")
print("|-------|------|----------|-------------------|")
callback_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=True)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuecallback_results.append(result)
callback_overhead_kb=result["callback_info"]["total_callback_overhead"] /1024ifresult["callback_info"] else0print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes | {callback_overhead_kb:.1f} KB |")
# Test without callbacksprint("\n📊 **Basic DAGs (No Callbacks)**")
print("| Tasks | Size | Per Task |")
print("|-------|------|----------|")
basic_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=False)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuebasic_results.append(result)
print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes |")
# Show callback field details for largest DAGifcallback_results:
largest=callback_results[-1]
iflargest["callback_info"]:
print(f"\n🔍 **Callback Analysis (100 tasks):**")
ci=largest["callback_info"]
print(f" Callback fields: {ci['callback_fields']}")
print(f" Per-task callback overhead: {ci['callback_overhead_per_task']} bytes")
print(f" Total callback overhead: {ci['total_callback_overhead']:,} bytes ({ci['total_callback_overhead']/1024:.1f} KB)")
print(f" Task size with callbacks: {ci['task_size']} bytes")
print(f" Task size without callbacks: {ci['task_size_without_callbacks']} bytes")
# Generate CSV for easy importprint(f"\n📋 **CSV Format (for PR description):**")
print("# callbacks")
forresultincallback_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},True")
print("# basic") forresultinbasic_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},False")
returncallback_results, basic_resultsdefmain():
"""Main function."""run_scaling_measurements()
if__name__=="__main__":
main()

Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 8baf823 to 0334138CompareAugust 20, 2025 14:58
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 8248e4a to c126079CompareAugust 23, 2025 22:52
@kaxilkaxil mentioned this pull request Aug 26, 2025
4 tasks
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 89a6807 to c0be635CompareAugust 27, 2025 07:29
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 6351823 to 3674170CompareAugust 28, 2025 19:42
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 7850b64 to 93c2642CompareAugust 28, 2025 22:58
@kaxilkaxil changed the title [DO NOT REVIEW] Remove Task SDK dependencies from airflow-core deserializationDecouple Serialization and Deserialization Code for OperatorsAug 28, 2025
@kaxilkaxil changed the title Decouple Serialization and Deserialization Code for OperatorsDecouple Serialization and Deserialization Code for tasksAug 28, 2025
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 93c2642 to 3bfc590CompareAugust 29, 2025 00:39
@kaxilkaxil added the full tests needed We need to run full set of tests for this PR to merge label Aug 29, 2025
@kaxil
kaxil marked this pull request as ready for review August 29, 2025 01:25
Comment threadairflow-core/docs/administration-and-deployment/dag-serialization.rst Outdated
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py Outdated
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 3bfc590 to eb97006CompareAugust 29, 2025 22:42
@kaxil
kaxil merged commit d9969be into apache:mainAug 29, 2025
107 checks passed
@kaxil
kaxil deleted the serialization/op-defaults branch August 29, 2025 23:29
mangal-vairalkar pushed a commit to mangal-vairalkar/airflow that referenced this pull request Aug 30, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
bggwak pushed a commit to bggwak/airflow that referenced this pull request Sep 2, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
(cherry picked from commit a582464)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:serializationarea:task-sdkfull tests neededWe need to run full set of tests for this PR to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@kaxil@jedcunningham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Decouple Serialization and Deserialization Code for tasks - #54569

Merged
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults
Aug 29, 2025
Merged

Decouple Serialization and Deserialization Code for tasks#54569
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults

Conversation

@kaxil

@kaxilkaxil commented Aug 16, 2025

Copy link
Copy Markdown
Member

🎯 Problem Statement

The Task SDK separation in Airflow 3.1 requires decoupling serialization and deserialization code to eliminate server-side dependencies on client SDK implementations:

  1. Task SDK Dependencies: airflow-core deserialization currently depends on Task SDK's BaseOperator for default values and field lists
  2. Architectural Coupling: Server components import and use Task SDK classes during deserialization, violating client/server separation
  3. Independent Deployment Blocker: Tight coupling prevents independent deployment and upgrade of server vs client components

🚀 Solution Overview

This PR decouples (to a great extent) serialization and deserialization code by removing Task SDK dependencies from airflow-core:

  • Remove dynamic SDK calls: Replace get_serialized_fields() calls with hardcoded class methods
  • Eliminate import dependencies: Remove OPERATOR_DEFAULTS and other Task SDK imports from server-side code
  • Schema-driven defaults: Use schema.json and client_defaults instead of Task SDK classes for default resolution
  • Independent deployment: Enable server/client components to be deployed and upgraded separately

📊 Benchmark

As part of this change, I optimised how the defaults are stored and when a field is stored and removed anything that matches defaults, nulls and the bigger impact change to remove storing entire callback functions as strings and instead store a boolean to indicate if a callback was set or not.

The bigger the DAG (more tasks + especially with callbacks), the more savings.

Using actual pre-optimization code:

ScenarioTasksBeforeAfterSavedReduction
Basic DAGs107.5 KB5.7 KB1.8 KB24.0%
5033.7 KB24.5 KB9.2 KB27.3%
10066.4 KB48.0 KB18.4 KB27.7%
Production DAGs (3 callbacks/task)1015.5 KB6.6 KB8.9 KB57.4%
5073.8 KB28.9 KB44.9 KB60.8%
100146.7 KB56.9 KB89.8 KB61.2%

🔥 Callback Optimization Analysis (100 tasks with 3 callbacks each):

Storage MethodBeforeAfterSavedReduction
Callback representationLists of function codeBoolean flags--
Per-task callback overhead822 bytes91 bytes731 bytes89%
Total callback overhead80.3 KB8.9 KB71.4 KB89%
Per-task total size1,502 bytes583 bytes919 bytes61%

🎯 Key Optimization Impact:

  • Callback transformation: 89% reduction in callback storage overhead (function code → boolean flags)
  • Production scaling: 57% → 61% reduction as DAG size increases (10 → 100 tasks)
  • Per-task efficiency: 919 bytes saved per task (1,502 → 583 bytes) for callback DAGs
  • Consistent baseline: 24-28% reduction even for basic DAGs without callbacks

🏗️ Architecture Changes

Task Default Resolution

Implements hierarchical defaults during deserialization:

  1. Schema defaults (from schema.json) - lowest priority
  2. client_defaults.tasks - SDK-specific overrides
  3. partial_kwargs - MappedOperator values
  4. Explicit task values - highest priority

Serialization Exclusion

Fields matching client_defaults are automatically excluded from task serialization, reducing redundancy while maintaining full information.

fyi: Following the Task Execution API pattern, I aim to add versioned schema contract at Airflow website directly or version docs soon'ish:

Thinking about a URL like: https://airflow.apache.org/schemas/dag-serialization/v2.json

🚦 Migration Path

For Users

  • No action required - changes are completely transparent
  • Existing DAGs continue working unchanged
  • New DAGs automatically benefit from optimizations

Appendix (for my own tracking)

TODOs (some might be done in a future PR):

  • Add defaults to schema.json
  • Exclude defaults in schema from Serialized JSON
  • Change on_*_callback on tasks to use has_on_*_callback
  • Remove unmap method from scheduler-side #54816
  • Implement client_defaults generation in serialization (Task SDK side)
  • Verify if the change is backwards compatible. If not, Update serialization version to v3 and add backwards compatibility for v2. Update: It is a backwards-compatible change
  • Add tests to ensure defaults in Schema are same as the Server side classes. Or better add prek/pre-commit to autogenerate default from server-side to Schema
  • Evaluate the alternative of storing the list of attributes needed for Serialization & De-serialization in schema.JSON

Future Work:

  • Move the S10n code over for DAG & Task Group classes to Server-side
  • Include schema.json in the calver OpenAPI spec for Execution API and/or in airflow versioned docs
  • Move Serialization code to Task SDK
  • Remove ui_color & ui_fgcolor

Other points

  • If we move serialization to Task SDK and keep de-serializtion to the Server side, how do we handle the following:
    • XCom deserialization -- it currently uses the airflow.serialization module
    • ExtendedJSON - TypeDecorator used in serialization of the following:
      • DagRun.context_carrier
      • TaskInstance.next_kwargs

Benchmark script:

#!/usr/bin/env python3"""Script to measure real serialization scaling for different DAG sizes.Uses actual JSON examination for accurate before/after comparison."""importjsonimportsysfromdatetimeimportdatetime, timedeltafrompathlibimportPathfromairflow.sdkimportDAGfromairflow.providers.standard.operators.bashimportBashOperatorfromairflow.serialization.serialized_objectsimportSerializedDAGdefsuccess_callback(context):
"""Example success callback function."""print(f"Task {context['task_instance'].task_id} succeeded!")
return"success"deffailure_callback(context):
"""Example failure callback function."""print(f"Task {context['task_instance'].task_id} failed!")
# Send notification to Slackimportrequestsrequests.post("https://hooks.slack.com/webhook", json={
"text": f"❌ Task failed: {context['task_instance'].task_id}"
})
return"failure_handled"defretry_callback(context):
"""Example retry callback function."""print(f"Task {context['task_instance'].task_id} will retry!")
return"retry_scheduled"defcreate_test_dag(num_tasks: int, with_callbacks: bool=True) ->DAG:
"""Create a test DAG with specified number of tasks."""dag=DAG(
dag_id=f"scaling_test_dag_{num_tasks}",
start_date=datetime(2024, 1, 1),
schedule="@daily",
default_args={
"owner": "test_user",
"retries": 2,
"retry_delay": timedelta(minutes=5),
"email": "test@example.com",
"email_on_failure": True,
"email_on_retry": True,
}
)
withdag:
foriinrange(num_tasks):
task_kwargs= {
"task_id": f"task_{i:03d}",
"bash_command": f"echo 'Processing item {i}'",
"email_on_failure": True,
"email_on_retry": True,
}
ifwith_callbacks:
task_kwargs.update({
"on_success_callback": success_callback,
"on_failure_callback": failure_callback,
"on_retry_callback": retry_callback,
})
BashOperator(**task_kwargs)
returndagdefmeasure_dag_serialization(num_tasks: int, with_callbacks: bool=True) ->dict:
"""Measure serialization for a DAG with specified parameters."""dag=create_test_dag(num_tasks, with_callbacks)
try:
serialized=SerializedDAG.to_dict(dag)
exceptExceptionase:
return {"error": str(e)}
# Convert to compact JSONjson_compact=json.dumps(serialized, separators=(',', ':'))
total_size=len(json_compact.encode('utf-8'))
# Analyze callback fields if callbacks are enabledcallback_info= {}
ifwith_callbacksand"dag"inserializedand"tasks"inserialized["dag"]:
tasks=serialized["dag"]["tasks"]
iftasks:
first_task=tasks[0]["__var"]
# Find callback fieldscallback_fields= [kforkinfirst_taskif"callback"ink.lower()]
# Calculate callback overheadtask_json=json.dumps(first_task, separators=(',', ':'))
total_task_size=len(task_json.encode('utf-8'))
task_without_callbacks= {k: vfork, vinfirst_task.items() if"callback"notink.lower()}
no_callback_json=json.dumps(task_without_callbacks, separators=(',', ':'))
no_callback_size=len(no_callback_json.encode('utf-8'))
callback_overhead_per_task=total_task_size-no_callback_sizetotal_callback_overhead=callback_overhead_per_task*len(tasks)
callback_info= {
"callback_fields": callback_fields,
"callback_overhead_per_task": callback_overhead_per_task,
"total_callback_overhead": total_callback_overhead,
"task_size": total_task_size,
"task_size_without_callbacks": no_callback_size
}
return {
"num_tasks": num_tasks,
"with_callbacks": with_callbacks,
"total_size": total_size,
"size_kb": total_size/1024,
"per_task_bytes": total_size/num_tasks,
"callback_info": callback_info
}
defrun_scaling_measurements():
"""Run measurements for different DAG sizes."""print("🔍 Measuring Real Serialization Scaling")
print("="*60)
task_counts= [10, 25, 50, 100]
# Test with callbacksprint("\n📊 **Production DAGs (With Callbacks)**")
print("| Tasks | Size | Per Task | Callback Overhead |")
print("|-------|------|----------|-------------------|")
callback_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=True)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuecallback_results.append(result)
callback_overhead_kb=result["callback_info"]["total_callback_overhead"] /1024ifresult["callback_info"] else0print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes | {callback_overhead_kb:.1f} KB |")
# Test without callbacksprint("\n📊 **Basic DAGs (No Callbacks)**")
print("| Tasks | Size | Per Task |")
print("|-------|------|----------|")
basic_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=False)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuebasic_results.append(result)
print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes |")
# Show callback field details for largest DAGifcallback_results:
largest=callback_results[-1]
iflargest["callback_info"]:
print(f"\n🔍 **Callback Analysis (100 tasks):**")
ci=largest["callback_info"]
print(f" Callback fields: {ci['callback_fields']}")
print(f" Per-task callback overhead: {ci['callback_overhead_per_task']} bytes")
print(f" Total callback overhead: {ci['total_callback_overhead']:,} bytes ({ci['total_callback_overhead']/1024:.1f} KB)")
print(f" Task size with callbacks: {ci['task_size']} bytes")
print(f" Task size without callbacks: {ci['task_size_without_callbacks']} bytes")
# Generate CSV for easy importprint(f"\n📋 **CSV Format (for PR description):**")
print("# callbacks")
forresultincallback_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},True")
print("# basic") forresultinbasic_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},False")
returncallback_results, basic_resultsdefmain():
"""Main function."""run_scaling_measurements()
if__name__=="__main__":
main()

Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 8baf823 to 0334138CompareAugust 20, 2025 14:58
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 8248e4a to c126079CompareAugust 23, 2025 22:52
@kaxilkaxil mentioned this pull request Aug 26, 2025
4 tasks
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 89a6807 to c0be635CompareAugust 27, 2025 07:29
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 6351823 to 3674170CompareAugust 28, 2025 19:42
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 7850b64 to 93c2642CompareAugust 28, 2025 22:58
@kaxilkaxil changed the title [DO NOT REVIEW] Remove Task SDK dependencies from airflow-core deserializationDecouple Serialization and Deserialization Code for OperatorsAug 28, 2025
@kaxilkaxil changed the title Decouple Serialization and Deserialization Code for OperatorsDecouple Serialization and Deserialization Code for tasksAug 28, 2025
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 93c2642 to 3bfc590CompareAugust 29, 2025 00:39
@kaxilkaxil added the full tests needed We need to run full set of tests for this PR to merge label Aug 29, 2025
@kaxil
kaxil marked this pull request as ready for review August 29, 2025 01:25
Comment threadairflow-core/docs/administration-and-deployment/dag-serialization.rst Outdated
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py Outdated
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 3bfc590 to eb97006CompareAugust 29, 2025 22:42
@kaxil
kaxil merged commit d9969be into apache:mainAug 29, 2025
107 checks passed
@kaxil
kaxil deleted the serialization/op-defaults branch August 29, 2025 23:29
mangal-vairalkar pushed a commit to mangal-vairalkar/airflow that referenced this pull request Aug 30, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
bggwak pushed a commit to bggwak/airflow that referenced this pull request Sep 2, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
(cherry picked from commit a582464)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:serializationarea:task-sdkfull tests neededWe need to run full set of tests for this PR to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@kaxil@jedcunningham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Decouple Serialization and Deserialization Code for tasks - #54569

Merged
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults
Aug 29, 2025
Merged

Decouple Serialization and Deserialization Code for tasks#54569
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults

Conversation

@kaxil

@kaxilkaxil commented Aug 16, 2025

Copy link
Copy Markdown
Member

🎯 Problem Statement

The Task SDK separation in Airflow 3.1 requires decoupling serialization and deserialization code to eliminate server-side dependencies on client SDK implementations:

  1. Task SDK Dependencies: airflow-core deserialization currently depends on Task SDK's BaseOperator for default values and field lists
  2. Architectural Coupling: Server components import and use Task SDK classes during deserialization, violating client/server separation
  3. Independent Deployment Blocker: Tight coupling prevents independent deployment and upgrade of server vs client components

🚀 Solution Overview

This PR decouples (to a great extent) serialization and deserialization code by removing Task SDK dependencies from airflow-core:

  • Remove dynamic SDK calls: Replace get_serialized_fields() calls with hardcoded class methods
  • Eliminate import dependencies: Remove OPERATOR_DEFAULTS and other Task SDK imports from server-side code
  • Schema-driven defaults: Use schema.json and client_defaults instead of Task SDK classes for default resolution
  • Independent deployment: Enable server/client components to be deployed and upgraded separately

📊 Benchmark

As part of this change, I optimised how the defaults are stored and when a field is stored and removed anything that matches defaults, nulls and the bigger impact change to remove storing entire callback functions as strings and instead store a boolean to indicate if a callback was set or not.

The bigger the DAG (more tasks + especially with callbacks), the more savings.

Using actual pre-optimization code:

ScenarioTasksBeforeAfterSavedReduction
Basic DAGs107.5 KB5.7 KB1.8 KB24.0%
5033.7 KB24.5 KB9.2 KB27.3%
10066.4 KB48.0 KB18.4 KB27.7%
Production DAGs (3 callbacks/task)1015.5 KB6.6 KB8.9 KB57.4%
5073.8 KB28.9 KB44.9 KB60.8%
100146.7 KB56.9 KB89.8 KB61.2%

🔥 Callback Optimization Analysis (100 tasks with 3 callbacks each):

Storage MethodBeforeAfterSavedReduction
Callback representationLists of function codeBoolean flags--
Per-task callback overhead822 bytes91 bytes731 bytes89%
Total callback overhead80.3 KB8.9 KB71.4 KB89%
Per-task total size1,502 bytes583 bytes919 bytes61%

🎯 Key Optimization Impact:

  • Callback transformation: 89% reduction in callback storage overhead (function code → boolean flags)
  • Production scaling: 57% → 61% reduction as DAG size increases (10 → 100 tasks)
  • Per-task efficiency: 919 bytes saved per task (1,502 → 583 bytes) for callback DAGs
  • Consistent baseline: 24-28% reduction even for basic DAGs without callbacks

🏗️ Architecture Changes

Task Default Resolution

Implements hierarchical defaults during deserialization:

  1. Schema defaults (from schema.json) - lowest priority
  2. client_defaults.tasks - SDK-specific overrides
  3. partial_kwargs - MappedOperator values
  4. Explicit task values - highest priority

Serialization Exclusion

Fields matching client_defaults are automatically excluded from task serialization, reducing redundancy while maintaining full information.

fyi: Following the Task Execution API pattern, I aim to add versioned schema contract at Airflow website directly or version docs soon'ish:

Thinking about a URL like: https://airflow.apache.org/schemas/dag-serialization/v2.json

🚦 Migration Path

For Users

  • No action required - changes are completely transparent
  • Existing DAGs continue working unchanged
  • New DAGs automatically benefit from optimizations

Appendix (for my own tracking)

TODOs (some might be done in a future PR):

  • Add defaults to schema.json
  • Exclude defaults in schema from Serialized JSON
  • Change on_*_callback on tasks to use has_on_*_callback
  • Remove unmap method from scheduler-side #54816
  • Implement client_defaults generation in serialization (Task SDK side)
  • Verify if the change is backwards compatible. If not, Update serialization version to v3 and add backwards compatibility for v2. Update: It is a backwards-compatible change
  • Add tests to ensure defaults in Schema are same as the Server side classes. Or better add prek/pre-commit to autogenerate default from server-side to Schema
  • Evaluate the alternative of storing the list of attributes needed for Serialization & De-serialization in schema.JSON

Future Work:

  • Move the S10n code over for DAG & Task Group classes to Server-side
  • Include schema.json in the calver OpenAPI spec for Execution API and/or in airflow versioned docs
  • Move Serialization code to Task SDK
  • Remove ui_color & ui_fgcolor

Other points

  • If we move serialization to Task SDK and keep de-serializtion to the Server side, how do we handle the following:
    • XCom deserialization -- it currently uses the airflow.serialization module
    • ExtendedJSON - TypeDecorator used in serialization of the following:
      • DagRun.context_carrier
      • TaskInstance.next_kwargs

Benchmark script:

#!/usr/bin/env python3"""Script to measure real serialization scaling for different DAG sizes.Uses actual JSON examination for accurate before/after comparison."""importjsonimportsysfromdatetimeimportdatetime, timedeltafrompathlibimportPathfromairflow.sdkimportDAGfromairflow.providers.standard.operators.bashimportBashOperatorfromairflow.serialization.serialized_objectsimportSerializedDAGdefsuccess_callback(context):
"""Example success callback function."""print(f"Task {context['task_instance'].task_id} succeeded!")
return"success"deffailure_callback(context):
"""Example failure callback function."""print(f"Task {context['task_instance'].task_id} failed!")
# Send notification to Slackimportrequestsrequests.post("https://hooks.slack.com/webhook", json={
"text": f"❌ Task failed: {context['task_instance'].task_id}"
})
return"failure_handled"defretry_callback(context):
"""Example retry callback function."""print(f"Task {context['task_instance'].task_id} will retry!")
return"retry_scheduled"defcreate_test_dag(num_tasks: int, with_callbacks: bool=True) ->DAG:
"""Create a test DAG with specified number of tasks."""dag=DAG(
dag_id=f"scaling_test_dag_{num_tasks}",
start_date=datetime(2024, 1, 1),
schedule="@daily",
default_args={
"owner": "test_user",
"retries": 2,
"retry_delay": timedelta(minutes=5),
"email": "test@example.com",
"email_on_failure": True,
"email_on_retry": True,
}
)
withdag:
foriinrange(num_tasks):
task_kwargs= {
"task_id": f"task_{i:03d}",
"bash_command": f"echo 'Processing item {i}'",
"email_on_failure": True,
"email_on_retry": True,
}
ifwith_callbacks:
task_kwargs.update({
"on_success_callback": success_callback,
"on_failure_callback": failure_callback,
"on_retry_callback": retry_callback,
})
BashOperator(**task_kwargs)
returndagdefmeasure_dag_serialization(num_tasks: int, with_callbacks: bool=True) ->dict:
"""Measure serialization for a DAG with specified parameters."""dag=create_test_dag(num_tasks, with_callbacks)
try:
serialized=SerializedDAG.to_dict(dag)
exceptExceptionase:
return {"error": str(e)}
# Convert to compact JSONjson_compact=json.dumps(serialized, separators=(',', ':'))
total_size=len(json_compact.encode('utf-8'))
# Analyze callback fields if callbacks are enabledcallback_info= {}
ifwith_callbacksand"dag"inserializedand"tasks"inserialized["dag"]:
tasks=serialized["dag"]["tasks"]
iftasks:
first_task=tasks[0]["__var"]
# Find callback fieldscallback_fields= [kforkinfirst_taskif"callback"ink.lower()]
# Calculate callback overheadtask_json=json.dumps(first_task, separators=(',', ':'))
total_task_size=len(task_json.encode('utf-8'))
task_without_callbacks= {k: vfork, vinfirst_task.items() if"callback"notink.lower()}
no_callback_json=json.dumps(task_without_callbacks, separators=(',', ':'))
no_callback_size=len(no_callback_json.encode('utf-8'))
callback_overhead_per_task=total_task_size-no_callback_sizetotal_callback_overhead=callback_overhead_per_task*len(tasks)
callback_info= {
"callback_fields": callback_fields,
"callback_overhead_per_task": callback_overhead_per_task,
"total_callback_overhead": total_callback_overhead,
"task_size": total_task_size,
"task_size_without_callbacks": no_callback_size
}
return {
"num_tasks": num_tasks,
"with_callbacks": with_callbacks,
"total_size": total_size,
"size_kb": total_size/1024,
"per_task_bytes": total_size/num_tasks,
"callback_info": callback_info
}
defrun_scaling_measurements():
"""Run measurements for different DAG sizes."""print("🔍 Measuring Real Serialization Scaling")
print("="*60)
task_counts= [10, 25, 50, 100]
# Test with callbacksprint("\n📊 **Production DAGs (With Callbacks)**")
print("| Tasks | Size | Per Task | Callback Overhead |")
print("|-------|------|----------|-------------------|")
callback_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=True)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuecallback_results.append(result)
callback_overhead_kb=result["callback_info"]["total_callback_overhead"] /1024ifresult["callback_info"] else0print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes | {callback_overhead_kb:.1f} KB |")
# Test without callbacksprint("\n📊 **Basic DAGs (No Callbacks)**")
print("| Tasks | Size | Per Task |")
print("|-------|------|----------|")
basic_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=False)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuebasic_results.append(result)
print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes |")
# Show callback field details for largest DAGifcallback_results:
largest=callback_results[-1]
iflargest["callback_info"]:
print(f"\n🔍 **Callback Analysis (100 tasks):**")
ci=largest["callback_info"]
print(f" Callback fields: {ci['callback_fields']}")
print(f" Per-task callback overhead: {ci['callback_overhead_per_task']} bytes")
print(f" Total callback overhead: {ci['total_callback_overhead']:,} bytes ({ci['total_callback_overhead']/1024:.1f} KB)")
print(f" Task size with callbacks: {ci['task_size']} bytes")
print(f" Task size without callbacks: {ci['task_size_without_callbacks']} bytes")
# Generate CSV for easy importprint(f"\n📋 **CSV Format (for PR description):**")
print("# callbacks")
forresultincallback_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},True")
print("# basic") forresultinbasic_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},False")
returncallback_results, basic_resultsdefmain():
"""Main function."""run_scaling_measurements()
if__name__=="__main__":
main()

Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 8baf823 to 0334138CompareAugust 20, 2025 14:58
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 8248e4a to c126079CompareAugust 23, 2025 22:52
@kaxilkaxil mentioned this pull request Aug 26, 2025
4 tasks
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 89a6807 to c0be635CompareAugust 27, 2025 07:29
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 6351823 to 3674170CompareAugust 28, 2025 19:42
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 7850b64 to 93c2642CompareAugust 28, 2025 22:58
@kaxilkaxil changed the title [DO NOT REVIEW] Remove Task SDK dependencies from airflow-core deserializationDecouple Serialization and Deserialization Code for OperatorsAug 28, 2025
@kaxilkaxil changed the title Decouple Serialization and Deserialization Code for OperatorsDecouple Serialization and Deserialization Code for tasksAug 28, 2025
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 93c2642 to 3bfc590CompareAugust 29, 2025 00:39
@kaxilkaxil added the full tests needed We need to run full set of tests for this PR to merge label Aug 29, 2025
@kaxil
kaxil marked this pull request as ready for review August 29, 2025 01:25
Comment threadairflow-core/docs/administration-and-deployment/dag-serialization.rst Outdated
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py Outdated
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 3bfc590 to eb97006CompareAugust 29, 2025 22:42
@kaxil
kaxil merged commit d9969be into apache:mainAug 29, 2025
107 checks passed
@kaxil
kaxil deleted the serialization/op-defaults branch August 29, 2025 23:29
mangal-vairalkar pushed a commit to mangal-vairalkar/airflow that referenced this pull request Aug 30, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
bggwak pushed a commit to bggwak/airflow that referenced this pull request Sep 2, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
(cherry picked from commit a582464)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:serializationarea:task-sdkfull tests neededWe need to run full set of tests for this PR to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@kaxil@jedcunningham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Decouple Serialization and Deserialization Code for tasks - #54569

Merged
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults
Aug 29, 2025
Merged

Decouple Serialization and Deserialization Code for tasks#54569
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults

Conversation

@kaxil

@kaxilkaxil commented Aug 16, 2025

Copy link
Copy Markdown
Member

🎯 Problem Statement

The Task SDK separation in Airflow 3.1 requires decoupling serialization and deserialization code to eliminate server-side dependencies on client SDK implementations:

  1. Task SDK Dependencies: airflow-core deserialization currently depends on Task SDK's BaseOperator for default values and field lists
  2. Architectural Coupling: Server components import and use Task SDK classes during deserialization, violating client/server separation
  3. Independent Deployment Blocker: Tight coupling prevents independent deployment and upgrade of server vs client components

🚀 Solution Overview

This PR decouples (to a great extent) serialization and deserialization code by removing Task SDK dependencies from airflow-core:

  • Remove dynamic SDK calls: Replace get_serialized_fields() calls with hardcoded class methods
  • Eliminate import dependencies: Remove OPERATOR_DEFAULTS and other Task SDK imports from server-side code
  • Schema-driven defaults: Use schema.json and client_defaults instead of Task SDK classes for default resolution
  • Independent deployment: Enable server/client components to be deployed and upgraded separately

📊 Benchmark

As part of this change, I optimised how the defaults are stored and when a field is stored and removed anything that matches defaults, nulls and the bigger impact change to remove storing entire callback functions as strings and instead store a boolean to indicate if a callback was set or not.

The bigger the DAG (more tasks + especially with callbacks), the more savings.

Using actual pre-optimization code:

ScenarioTasksBeforeAfterSavedReduction
Basic DAGs107.5 KB5.7 KB1.8 KB24.0%
5033.7 KB24.5 KB9.2 KB27.3%
10066.4 KB48.0 KB18.4 KB27.7%
Production DAGs (3 callbacks/task)1015.5 KB6.6 KB8.9 KB57.4%
5073.8 KB28.9 KB44.9 KB60.8%
100146.7 KB56.9 KB89.8 KB61.2%

🔥 Callback Optimization Analysis (100 tasks with 3 callbacks each):

Storage MethodBeforeAfterSavedReduction
Callback representationLists of function codeBoolean flags--
Per-task callback overhead822 bytes91 bytes731 bytes89%
Total callback overhead80.3 KB8.9 KB71.4 KB89%
Per-task total size1,502 bytes583 bytes919 bytes61%

🎯 Key Optimization Impact:

  • Callback transformation: 89% reduction in callback storage overhead (function code → boolean flags)
  • Production scaling: 57% → 61% reduction as DAG size increases (10 → 100 tasks)
  • Per-task efficiency: 919 bytes saved per task (1,502 → 583 bytes) for callback DAGs
  • Consistent baseline: 24-28% reduction even for basic DAGs without callbacks

🏗️ Architecture Changes

Task Default Resolution

Implements hierarchical defaults during deserialization:

  1. Schema defaults (from schema.json) - lowest priority
  2. client_defaults.tasks - SDK-specific overrides
  3. partial_kwargs - MappedOperator values
  4. Explicit task values - highest priority

Serialization Exclusion

Fields matching client_defaults are automatically excluded from task serialization, reducing redundancy while maintaining full information.

fyi: Following the Task Execution API pattern, I aim to add versioned schema contract at Airflow website directly or version docs soon'ish:

Thinking about a URL like: https://airflow.apache.org/schemas/dag-serialization/v2.json

🚦 Migration Path

For Users

  • No action required - changes are completely transparent
  • Existing DAGs continue working unchanged
  • New DAGs automatically benefit from optimizations

Appendix (for my own tracking)

TODOs (some might be done in a future PR):

  • Add defaults to schema.json
  • Exclude defaults in schema from Serialized JSON
  • Change on_*_callback on tasks to use has_on_*_callback
  • Remove unmap method from scheduler-side #54816
  • Implement client_defaults generation in serialization (Task SDK side)
  • Verify if the change is backwards compatible. If not, Update serialization version to v3 and add backwards compatibility for v2. Update: It is a backwards-compatible change
  • Add tests to ensure defaults in Schema are same as the Server side classes. Or better add prek/pre-commit to autogenerate default from server-side to Schema
  • Evaluate the alternative of storing the list of attributes needed for Serialization & De-serialization in schema.JSON

Future Work:

  • Move the S10n code over for DAG & Task Group classes to Server-side
  • Include schema.json in the calver OpenAPI spec for Execution API and/or in airflow versioned docs
  • Move Serialization code to Task SDK
  • Remove ui_color & ui_fgcolor

Other points

  • If we move serialization to Task SDK and keep de-serializtion to the Server side, how do we handle the following:
    • XCom deserialization -- it currently uses the airflow.serialization module
    • ExtendedJSON - TypeDecorator used in serialization of the following:
      • DagRun.context_carrier
      • TaskInstance.next_kwargs

Benchmark script:

#!/usr/bin/env python3"""Script to measure real serialization scaling for different DAG sizes.Uses actual JSON examination for accurate before/after comparison."""importjsonimportsysfromdatetimeimportdatetime, timedeltafrompathlibimportPathfromairflow.sdkimportDAGfromairflow.providers.standard.operators.bashimportBashOperatorfromairflow.serialization.serialized_objectsimportSerializedDAGdefsuccess_callback(context):
"""Example success callback function."""print(f"Task {context['task_instance'].task_id} succeeded!")
return"success"deffailure_callback(context):
"""Example failure callback function."""print(f"Task {context['task_instance'].task_id} failed!")
# Send notification to Slackimportrequestsrequests.post("https://hooks.slack.com/webhook", json={
"text": f"❌ Task failed: {context['task_instance'].task_id}"
})
return"failure_handled"defretry_callback(context):
"""Example retry callback function."""print(f"Task {context['task_instance'].task_id} will retry!")
return"retry_scheduled"defcreate_test_dag(num_tasks: int, with_callbacks: bool=True) ->DAG:
"""Create a test DAG with specified number of tasks."""dag=DAG(
dag_id=f"scaling_test_dag_{num_tasks}",
start_date=datetime(2024, 1, 1),
schedule="@daily",
default_args={
"owner": "test_user",
"retries": 2,
"retry_delay": timedelta(minutes=5),
"email": "test@example.com",
"email_on_failure": True,
"email_on_retry": True,
}
)
withdag:
foriinrange(num_tasks):
task_kwargs= {
"task_id": f"task_{i:03d}",
"bash_command": f"echo 'Processing item {i}'",
"email_on_failure": True,
"email_on_retry": True,
}
ifwith_callbacks:
task_kwargs.update({
"on_success_callback": success_callback,
"on_failure_callback": failure_callback,
"on_retry_callback": retry_callback,
})
BashOperator(**task_kwargs)
returndagdefmeasure_dag_serialization(num_tasks: int, with_callbacks: bool=True) ->dict:
"""Measure serialization for a DAG with specified parameters."""dag=create_test_dag(num_tasks, with_callbacks)
try:
serialized=SerializedDAG.to_dict(dag)
exceptExceptionase:
return {"error": str(e)}
# Convert to compact JSONjson_compact=json.dumps(serialized, separators=(',', ':'))
total_size=len(json_compact.encode('utf-8'))
# Analyze callback fields if callbacks are enabledcallback_info= {}
ifwith_callbacksand"dag"inserializedand"tasks"inserialized["dag"]:
tasks=serialized["dag"]["tasks"]
iftasks:
first_task=tasks[0]["__var"]
# Find callback fieldscallback_fields= [kforkinfirst_taskif"callback"ink.lower()]
# Calculate callback overheadtask_json=json.dumps(first_task, separators=(',', ':'))
total_task_size=len(task_json.encode('utf-8'))
task_without_callbacks= {k: vfork, vinfirst_task.items() if"callback"notink.lower()}
no_callback_json=json.dumps(task_without_callbacks, separators=(',', ':'))
no_callback_size=len(no_callback_json.encode('utf-8'))
callback_overhead_per_task=total_task_size-no_callback_sizetotal_callback_overhead=callback_overhead_per_task*len(tasks)
callback_info= {
"callback_fields": callback_fields,
"callback_overhead_per_task": callback_overhead_per_task,
"total_callback_overhead": total_callback_overhead,
"task_size": total_task_size,
"task_size_without_callbacks": no_callback_size
}
return {
"num_tasks": num_tasks,
"with_callbacks": with_callbacks,
"total_size": total_size,
"size_kb": total_size/1024,
"per_task_bytes": total_size/num_tasks,
"callback_info": callback_info
}
defrun_scaling_measurements():
"""Run measurements for different DAG sizes."""print("🔍 Measuring Real Serialization Scaling")
print("="*60)
task_counts= [10, 25, 50, 100]
# Test with callbacksprint("\n📊 **Production DAGs (With Callbacks)**")
print("| Tasks | Size | Per Task | Callback Overhead |")
print("|-------|------|----------|-------------------|")
callback_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=True)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuecallback_results.append(result)
callback_overhead_kb=result["callback_info"]["total_callback_overhead"] /1024ifresult["callback_info"] else0print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes | {callback_overhead_kb:.1f} KB |")
# Test without callbacksprint("\n📊 **Basic DAGs (No Callbacks)**")
print("| Tasks | Size | Per Task |")
print("|-------|------|----------|")
basic_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=False)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuebasic_results.append(result)
print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes |")
# Show callback field details for largest DAGifcallback_results:
largest=callback_results[-1]
iflargest["callback_info"]:
print(f"\n🔍 **Callback Analysis (100 tasks):**")
ci=largest["callback_info"]
print(f" Callback fields: {ci['callback_fields']}")
print(f" Per-task callback overhead: {ci['callback_overhead_per_task']} bytes")
print(f" Total callback overhead: {ci['total_callback_overhead']:,} bytes ({ci['total_callback_overhead']/1024:.1f} KB)")
print(f" Task size with callbacks: {ci['task_size']} bytes")
print(f" Task size without callbacks: {ci['task_size_without_callbacks']} bytes")
# Generate CSV for easy importprint(f"\n📋 **CSV Format (for PR description):**")
print("# callbacks")
forresultincallback_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},True")
print("# basic") forresultinbasic_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},False")
returncallback_results, basic_resultsdefmain():
"""Main function."""run_scaling_measurements()
if__name__=="__main__":
main()

Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 8baf823 to 0334138CompareAugust 20, 2025 14:58
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 8248e4a to c126079CompareAugust 23, 2025 22:52
@kaxilkaxil mentioned this pull request Aug 26, 2025
4 tasks
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 89a6807 to c0be635CompareAugust 27, 2025 07:29
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 6351823 to 3674170CompareAugust 28, 2025 19:42
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 7850b64 to 93c2642CompareAugust 28, 2025 22:58
@kaxilkaxil changed the title [DO NOT REVIEW] Remove Task SDK dependencies from airflow-core deserializationDecouple Serialization and Deserialization Code for OperatorsAug 28, 2025
@kaxilkaxil changed the title Decouple Serialization and Deserialization Code for OperatorsDecouple Serialization and Deserialization Code for tasksAug 28, 2025
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 93c2642 to 3bfc590CompareAugust 29, 2025 00:39
@kaxilkaxil added the full tests needed We need to run full set of tests for this PR to merge label Aug 29, 2025
@kaxil
kaxil marked this pull request as ready for review August 29, 2025 01:25
Comment threadairflow-core/docs/administration-and-deployment/dag-serialization.rst Outdated
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py Outdated
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 3bfc590 to eb97006CompareAugust 29, 2025 22:42
@kaxil
kaxil merged commit d9969be into apache:mainAug 29, 2025
107 checks passed
@kaxil
kaxil deleted the serialization/op-defaults branch August 29, 2025 23:29
mangal-vairalkar pushed a commit to mangal-vairalkar/airflow that referenced this pull request Aug 30, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
bggwak pushed a commit to bggwak/airflow that referenced this pull request Sep 2, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
(cherry picked from commit a582464)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:serializationarea:task-sdkfull tests neededWe need to run full set of tests for this PR to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@kaxil@jedcunningham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Decouple Serialization and Deserialization Code for tasks - #54569

Merged
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults
Aug 29, 2025
Merged

Decouple Serialization and Deserialization Code for tasks#54569
kaxil merged 1 commit into
apache:mainfrom
astronomer:serialization/op-defaults

Conversation

@kaxil

@kaxilkaxil commented Aug 16, 2025

Copy link
Copy Markdown
Member

🎯 Problem Statement

The Task SDK separation in Airflow 3.1 requires decoupling serialization and deserialization code to eliminate server-side dependencies on client SDK implementations:

  1. Task SDK Dependencies: airflow-core deserialization currently depends on Task SDK's BaseOperator for default values and field lists
  2. Architectural Coupling: Server components import and use Task SDK classes during deserialization, violating client/server separation
  3. Independent Deployment Blocker: Tight coupling prevents independent deployment and upgrade of server vs client components

🚀 Solution Overview

This PR decouples (to a great extent) serialization and deserialization code by removing Task SDK dependencies from airflow-core:

  • Remove dynamic SDK calls: Replace get_serialized_fields() calls with hardcoded class methods
  • Eliminate import dependencies: Remove OPERATOR_DEFAULTS and other Task SDK imports from server-side code
  • Schema-driven defaults: Use schema.json and client_defaults instead of Task SDK classes for default resolution
  • Independent deployment: Enable server/client components to be deployed and upgraded separately

📊 Benchmark

As part of this change, I optimised how the defaults are stored and when a field is stored and removed anything that matches defaults, nulls and the bigger impact change to remove storing entire callback functions as strings and instead store a boolean to indicate if a callback was set or not.

The bigger the DAG (more tasks + especially with callbacks), the more savings.

Using actual pre-optimization code:

ScenarioTasksBeforeAfterSavedReduction
Basic DAGs107.5 KB5.7 KB1.8 KB24.0%
5033.7 KB24.5 KB9.2 KB27.3%
10066.4 KB48.0 KB18.4 KB27.7%
Production DAGs (3 callbacks/task)1015.5 KB6.6 KB8.9 KB57.4%
5073.8 KB28.9 KB44.9 KB60.8%
100146.7 KB56.9 KB89.8 KB61.2%

🔥 Callback Optimization Analysis (100 tasks with 3 callbacks each):

Storage MethodBeforeAfterSavedReduction
Callback representationLists of function codeBoolean flags--
Per-task callback overhead822 bytes91 bytes731 bytes89%
Total callback overhead80.3 KB8.9 KB71.4 KB89%
Per-task total size1,502 bytes583 bytes919 bytes61%

🎯 Key Optimization Impact:

  • Callback transformation: 89% reduction in callback storage overhead (function code → boolean flags)
  • Production scaling: 57% → 61% reduction as DAG size increases (10 → 100 tasks)
  • Per-task efficiency: 919 bytes saved per task (1,502 → 583 bytes) for callback DAGs
  • Consistent baseline: 24-28% reduction even for basic DAGs without callbacks

🏗️ Architecture Changes

Task Default Resolution

Implements hierarchical defaults during deserialization:

  1. Schema defaults (from schema.json) - lowest priority
  2. client_defaults.tasks - SDK-specific overrides
  3. partial_kwargs - MappedOperator values
  4. Explicit task values - highest priority

Serialization Exclusion

Fields matching client_defaults are automatically excluded from task serialization, reducing redundancy while maintaining full information.

fyi: Following the Task Execution API pattern, I aim to add versioned schema contract at Airflow website directly or version docs soon'ish:

Thinking about a URL like: https://airflow.apache.org/schemas/dag-serialization/v2.json

🚦 Migration Path

For Users

  • No action required - changes are completely transparent
  • Existing DAGs continue working unchanged
  • New DAGs automatically benefit from optimizations

Appendix (for my own tracking)

TODOs (some might be done in a future PR):

  • Add defaults to schema.json
  • Exclude defaults in schema from Serialized JSON
  • Change on_*_callback on tasks to use has_on_*_callback
  • Remove unmap method from scheduler-side #54816
  • Implement client_defaults generation in serialization (Task SDK side)
  • Verify if the change is backwards compatible. If not, Update serialization version to v3 and add backwards compatibility for v2. Update: It is a backwards-compatible change
  • Add tests to ensure defaults in Schema are same as the Server side classes. Or better add prek/pre-commit to autogenerate default from server-side to Schema
  • Evaluate the alternative of storing the list of attributes needed for Serialization & De-serialization in schema.JSON

Future Work:

  • Move the S10n code over for DAG & Task Group classes to Server-side
  • Include schema.json in the calver OpenAPI spec for Execution API and/or in airflow versioned docs
  • Move Serialization code to Task SDK
  • Remove ui_color & ui_fgcolor

Other points

  • If we move serialization to Task SDK and keep de-serializtion to the Server side, how do we handle the following:
    • XCom deserialization -- it currently uses the airflow.serialization module
    • ExtendedJSON - TypeDecorator used in serialization of the following:
      • DagRun.context_carrier
      • TaskInstance.next_kwargs

Benchmark script:

#!/usr/bin/env python3"""Script to measure real serialization scaling for different DAG sizes.Uses actual JSON examination for accurate before/after comparison."""importjsonimportsysfromdatetimeimportdatetime, timedeltafrompathlibimportPathfromairflow.sdkimportDAGfromairflow.providers.standard.operators.bashimportBashOperatorfromairflow.serialization.serialized_objectsimportSerializedDAGdefsuccess_callback(context):
"""Example success callback function."""print(f"Task {context['task_instance'].task_id} succeeded!")
return"success"deffailure_callback(context):
"""Example failure callback function."""print(f"Task {context['task_instance'].task_id} failed!")
# Send notification to Slackimportrequestsrequests.post("https://hooks.slack.com/webhook", json={
"text": f"❌ Task failed: {context['task_instance'].task_id}"
})
return"failure_handled"defretry_callback(context):
"""Example retry callback function."""print(f"Task {context['task_instance'].task_id} will retry!")
return"retry_scheduled"defcreate_test_dag(num_tasks: int, with_callbacks: bool=True) ->DAG:
"""Create a test DAG with specified number of tasks."""dag=DAG(
dag_id=f"scaling_test_dag_{num_tasks}",
start_date=datetime(2024, 1, 1),
schedule="@daily",
default_args={
"owner": "test_user",
"retries": 2,
"retry_delay": timedelta(minutes=5),
"email": "test@example.com",
"email_on_failure": True,
"email_on_retry": True,
}
)
withdag:
foriinrange(num_tasks):
task_kwargs= {
"task_id": f"task_{i:03d}",
"bash_command": f"echo 'Processing item {i}'",
"email_on_failure": True,
"email_on_retry": True,
}
ifwith_callbacks:
task_kwargs.update({
"on_success_callback": success_callback,
"on_failure_callback": failure_callback,
"on_retry_callback": retry_callback,
})
BashOperator(**task_kwargs)
returndagdefmeasure_dag_serialization(num_tasks: int, with_callbacks: bool=True) ->dict:
"""Measure serialization for a DAG with specified parameters."""dag=create_test_dag(num_tasks, with_callbacks)
try:
serialized=SerializedDAG.to_dict(dag)
exceptExceptionase:
return {"error": str(e)}
# Convert to compact JSONjson_compact=json.dumps(serialized, separators=(',', ':'))
total_size=len(json_compact.encode('utf-8'))
# Analyze callback fields if callbacks are enabledcallback_info= {}
ifwith_callbacksand"dag"inserializedand"tasks"inserialized["dag"]:
tasks=serialized["dag"]["tasks"]
iftasks:
first_task=tasks[0]["__var"]
# Find callback fieldscallback_fields= [kforkinfirst_taskif"callback"ink.lower()]
# Calculate callback overheadtask_json=json.dumps(first_task, separators=(',', ':'))
total_task_size=len(task_json.encode('utf-8'))
task_without_callbacks= {k: vfork, vinfirst_task.items() if"callback"notink.lower()}
no_callback_json=json.dumps(task_without_callbacks, separators=(',', ':'))
no_callback_size=len(no_callback_json.encode('utf-8'))
callback_overhead_per_task=total_task_size-no_callback_sizetotal_callback_overhead=callback_overhead_per_task*len(tasks)
callback_info= {
"callback_fields": callback_fields,
"callback_overhead_per_task": callback_overhead_per_task,
"total_callback_overhead": total_callback_overhead,
"task_size": total_task_size,
"task_size_without_callbacks": no_callback_size
}
return {
"num_tasks": num_tasks,
"with_callbacks": with_callbacks,
"total_size": total_size,
"size_kb": total_size/1024,
"per_task_bytes": total_size/num_tasks,
"callback_info": callback_info
}
defrun_scaling_measurements():
"""Run measurements for different DAG sizes."""print("🔍 Measuring Real Serialization Scaling")
print("="*60)
task_counts= [10, 25, 50, 100]
# Test with callbacksprint("\n📊 **Production DAGs (With Callbacks)**")
print("| Tasks | Size | Per Task | Callback Overhead |")
print("|-------|------|----------|-------------------|")
callback_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=True)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuecallback_results.append(result)
callback_overhead_kb=result["callback_info"]["total_callback_overhead"] /1024ifresult["callback_info"] else0print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes | {callback_overhead_kb:.1f} KB |")
# Test without callbacksprint("\n📊 **Basic DAGs (No Callbacks)**")
print("| Tasks | Size | Per Task |")
print("|-------|------|----------|")
basic_results= []
fornum_tasksintask_counts:
result=measure_dag_serialization(num_tasks, with_callbacks=False)
if"error"inresult:
print(f"| {num_tasks} | ERROR: {result['error']} |")
continuebasic_results.append(result)
print(f"| {num_tasks} | {result['size_kb']:.1f} KB | {result['per_task_bytes']:.0f} bytes |")
# Show callback field details for largest DAGifcallback_results:
largest=callback_results[-1]
iflargest["callback_info"]:
print(f"\n🔍 **Callback Analysis (100 tasks):**")
ci=largest["callback_info"]
print(f" Callback fields: {ci['callback_fields']}")
print(f" Per-task callback overhead: {ci['callback_overhead_per_task']} bytes")
print(f" Total callback overhead: {ci['total_callback_overhead']:,} bytes ({ci['total_callback_overhead']/1024:.1f} KB)")
print(f" Task size with callbacks: {ci['task_size']} bytes")
print(f" Task size without callbacks: {ci['task_size_without_callbacks']} bytes")
# Generate CSV for easy importprint(f"\n📋 **CSV Format (for PR description):**")
print("# callbacks")
forresultincallback_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},True")
print("# basic") forresultinbasic_results:
print(f"{result['num_tasks']},{result['size_kb']:.1f},{result['per_task_bytes']:.0f},False")
returncallback_results, basic_resultsdefmain():
"""Main function."""run_scaling_measurements()
if__name__=="__main__":
main()

Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 8baf823 to 0334138CompareAugust 20, 2025 14:58
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 8248e4a to c126079CompareAugust 23, 2025 22:52
@kaxilkaxil mentioned this pull request Aug 26, 2025
4 tasks
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 89a6807 to c0be635CompareAugust 27, 2025 07:29
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 5 times, most recently from 6351823 to 3674170CompareAugust 28, 2025 19:42
@kaxil
kaxilforce-pushed the serialization/op-defaults branch 2 times, most recently from 7850b64 to 93c2642CompareAugust 28, 2025 22:58
@kaxilkaxil changed the title [DO NOT REVIEW] Remove Task SDK dependencies from airflow-core deserializationDecouple Serialization and Deserialization Code for OperatorsAug 28, 2025
@kaxilkaxil changed the title Decouple Serialization and Deserialization Code for OperatorsDecouple Serialization and Deserialization Code for tasksAug 28, 2025
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 93c2642 to 3bfc590CompareAugust 29, 2025 00:39
@kaxilkaxil added the full tests needed We need to run full set of tests for this PR to merge label Aug 29, 2025
@kaxil
kaxil marked this pull request as ready for review August 29, 2025 01:25
Comment threadairflow-core/docs/administration-and-deployment/dag-serialization.rst Outdated
Comment threadairflow-core/src/airflow/serialization/schema.json Outdated
Comment threadairflow-core/src/airflow/serialization/serialized_objects.py Outdated
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
@kaxil
kaxilforce-pushed the serialization/op-defaults branch from 3bfc590 to eb97006CompareAugust 29, 2025 22:42
@kaxil
kaxil merged commit d9969be into apache:mainAug 29, 2025
107 checks passed
@kaxil
kaxil deleted the serialization/op-defaults branch August 29, 2025 23:29
mangal-vairalkar pushed a commit to mangal-vairalkar/airflow that referenced this pull request Aug 30, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
bggwak pushed a commit to bggwak/airflow that referenced this pull request Sep 2, 2025
Remove Task SDK dependencies from airflow-core deserialization by establishing
a schema-based contract between client and server components. This
change enables independent deployment and upgrades while laying the foundation
for multi-language SDK support.
Key Decoupling Achievements:
- Replace dynamic get_serialized_fields() calls with hardcoded class methods
- Add schema-driven default resolution with get_operator_defaults_from_schema()
- Remove OPERATOR_DEFAULTS import dependency from airflow-core
- Implement SerializedBaseOperator class attributes for all operator defaults
- Update _is_excluded() logic to use schema defaults for efficient serialization
Serialization Optimizations:
- Unified partial_kwargs optimization supporting both encoded/non-encoded formats
- Intelligent default exclusion reducing storage redundancy
- MappedOperator.operator_class memory optimization (~90-95% reduction)
- Comprehensive client_defaults system with hierarchical resolution
Compatibility & Performance:
- Significant size reduction for typical DAGs with mapped operators
- Minimal overhead for client_defaults section (excellent efficiency)
- All existing serialized DAGs continue to work unchanged
Technical Implementation:
- Add generate_client_defaults() with LRU caching for optimal performance
- Implement _deserialize_partial_kwargs() supporting dual formats
- Centralized field deserialization eliminating code duplication
- Consolidated preprocessing logic in _preprocess_encoded_operator()
- Callback field preprocessing for backward compatibility
Testing & Validation:
- Added TestMappedOperatorSerializationAndClientDefaults with 9 comprehensive tests
- Parameterized tests for multiple serialization formats
- End-to-end validation of serialization/deserialization workflows
- Backward compatibility validation for callback field migration
This decoupling enables independent deployment/upgrades and provides the
foundation for multi-language SDK ecosystem alongside the Task Execution API.
Part of apache#45428
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit to astronomer/airflow that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of apache#54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
kaxil added a commit that referenced this pull request Sep 18, 2025
This change reduces serialized DAG size by automatically excluding fields
that match their schema default values, similar to how operator serialization
works. Fields like `catchup=False`, `max_active_runs=16`, and `fail_fast=False`
are no longer stored when they have default values.
Follow-up of #54569
(cherry picked from commit a582464)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:serializationarea:task-sdkfull tests neededWe need to run full set of tests for this PR to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@kaxil@jedcunningham