This is an automated email from the ASF dual-hosted git repository.

Miretpl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 68e1eb4557f Parse additional Kafka, Redis & SQS options in 
`broker_transport_options` (#70228)
68e1eb4557f is described below

commit 68e1eb4557f670c78ef2b5b9abb34abe2691182b
Author: stephen-bracken <[email protected]>
AuthorDate: Wed Aug 19 16:50:06 2026 +0100

    Parse additional Kafka, Redis & SQS options in `broker_transport_options` 
(#70228)
    
    * parse additional dict options in broker_transport_options
    
    * Apply suggestions from code review
    
    * remove formatting from `fetch_message_attributes` example
    * change `AirflowException` to `ValueError`
    
    Co-authored-by: Przemysław Mirowski 
<[email protected]>
    
    * update type hinting
    
    * add predefined_queues test and config doc
    
    * fix mypy and linting
    
    * Update exception messages
    
    Co-authored-by: Przemysław Mirowski 
<[email protected]>
    
    * fix linting
    
    ---------
    
    Co-authored-by: Stephen Bracken <email-protected>
    Co-authored-by: Przemysław Mirowski 
<[email protected]>
---
 generated/known_airflow_exceptions.txt             |   2 +-
 providers/celery/provider.yaml                     |  71 ++++++++++
 .../providers/celery/executors/default_celery.py   |  95 ++++++++++----
 .../airflow/providers/celery/get_provider_info.py  |  70 ++++++++++
 .../unit/celery/executors/test_celery_executor.py  | 144 ++++++++++++++++++++-
 5 files changed, 351 insertions(+), 31 deletions(-)

diff --git a/generated/known_airflow_exceptions.txt 
b/generated/known_airflow_exceptions.txt
index 6866f875992..acfd9ae3eb8 100644
--- a/generated/known_airflow_exceptions.txt
+++ b/generated/known_airflow_exceptions.txt
@@ -148,7 +148,7 @@ 
providers/apache/spark/src/airflow/providers/apache/spark/hooks/spark_submit.py:
 providers/arangodb/src/airflow/providers/arangodb/hooks/arangodb.py::9
 providers/atlassian/jira/src/airflow/providers/atlassian/jira/hooks/jira.py::1
 
providers/celery/src/airflow/providers/celery/executors/celery_executor_utils.py::2
-providers/celery/src/airflow/providers/celery/executors/default_celery.py::2
+providers/celery/src/airflow/providers/celery/executors/default_celery.py::1
 providers/celery/tests/integration/celery/test_celery_executor.py::2
 providers/cloudant/src/airflow/providers/cloudant/hooks/cloudant.py::2
 
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py::3
diff --git a/providers/celery/provider.yaml b/providers/celery/provider.yaml
index dfe7e2e4ed9..bdde5389f8d 100644
--- a/providers/celery/provider.yaml
+++ b/providers/celery/provider.yaml
@@ -460,6 +460,49 @@ config:
       Redis Sentinel as the result backend. See:
       
https://docs.celeryq.dev/en/latest/userguide/configuration.html#std:setting-result_backend_transport_options
     options:
+      client-config:
+        description: |
+          SQS botocore client config.
+        version_added: "3.24.0"
+        type: string
+        example: '{"connect_timeout": 5}'
+        default: ~
+      fetch_message_attributes:
+        description: |
+          SQS fetch message attributes.
+        version_added: "3.24.0"
+        type: string
+        example: '{"MessageSystemAttributeNames": ["SenderId", 
"SentTimestamp"]}'
+        default: ~
+      kafka_admin_config:
+        description: |
+          Kafka admin only config, see ``kafka_common_config``.
+        version_added: "3.24.0"
+        type: string
+        example: ~
+        default: ~
+      kafka_common_config:
+        description: |
+          Kafka config common to producers and consumers,
+          see 
https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/kafka.html.
+        version_added: "3.24.0"
+        type: string
+        example: '{"bootstrap.servers": "broker:9094"}'
+        default: ~
+      kafka_consumer_config:
+        description: |
+          Kafka consumer specific config, see ``kafka_common_config``.
+        version_added: "3.24.0"
+        type: string
+        example: ~
+        default: ~
+      kafka_producer_config:
+        description: |
+          Kafka producer specific config, see ``kafka_common_config``.
+        version_added: "3.24.0"
+        type: string
+        example: ~
+        default: ~
       master_name:
         description: |
           The name of the Redis Sentinel primary node to connect to.
@@ -468,6 +511,27 @@ config:
         type: string
         example: "mymaster"
         default: ~
+      predefined_exchanges:
+        description: |
+          SQS Predefined SNS topics.
+        version_added: "3.24.0"
+        type: string
+        example: '{"exchange-1": {"arn": 
"arn:aws:sns:us-east-1:xxx:exchange-1"}}'
+        default: ~
+      predefined_queues:
+        description:
+          SQS Predefined Queues.
+        version_added: "3.24.0"
+        type: string
+        example: '{"queue-1": {"url": 
"https://sqs.us-east-1.amazonaws.com/xxx/aaa"}}'
+        default: ~
+      queue_tags:
+        description: |
+          SQS Queue tags to apply during queue creation.
+        version_added: "3.24.0"
+        type: string
+        example: '{"Environment": "production", "Team": "backend"}'
+        default: ~
       sentinel_kwargs:
         description: |
           The sentinel_kwargs parameter allows passing additional options to 
the Sentinel client
@@ -482,3 +546,10 @@ config:
         sensitive: true
         example: '{"password": "password_for_redis_server"}'
         default: ~
+      sqs-creation-attributes:
+        description: |
+          Additional SQS queue attributes to apply as key/value pairs.
+        version_added: "3.24.0"
+        type: string
+        example: '{"KmsMasterKeyId": "alias/aws/sqs"}'
+        default: ~
diff --git 
a/providers/celery/src/airflow/providers/celery/executors/default_celery.py 
b/providers/celery/src/airflow/providers/celery/executors/default_celery.py
index 3af3aa7744e..c5cd8773d5f 100644
--- a/providers/celery/src/airflow/providers/celery/executors/default_celery.py
+++ b/providers/celery/src/airflow/providers/celery/executors/default_celery.py
@@ -23,12 +23,17 @@ import json
 import logging
 import re
 import ssl
-from typing import Any
+from typing import TYPE_CHECKING
 
 from airflow.exceptions import AirflowConfigException
 from airflow.providers.celery.version_compat import AIRFLOW_V_3_0_PLUS
 from airflow.providers.common.compat.sdk import AirflowException, conf
 
+if TYPE_CHECKING:
+    from typing import Any
+
+    from airflow.sdk.configuration import AirflowSDKConfigParser
+
 log = logging.getLogger(__name__)
 
 _USE_PSYCOPG3: bool
@@ -44,11 +49,70 @@ except (ImportError, ModuleNotFoundError):
     _USE_PSYCOPG3 = False
 
 
+# broker_transport_options accessed as dict
+# e.g. 
https://github.com/celery/kombu/blob/4281680ef3a275a7d87433a703790251d9805803/kombu/transport/confluentkafka.py#L338
+_BROKER_TRANSPORT_DICT_OPTIONS = [
+    "client-config",
+    "fetch_message_attributes",
+    "kafka_admin_config",
+    "kafka_common_config",
+    "kafka_consumer_config",
+    "kafka_producer_config",
+    "predefined_exchanges",
+    "predefined_queues",
+    "queue_tags",
+    "sentinel_kwargs",
+    "sqs-creation-attributes",
+]
+
+
 def _broker_supports_visibility_timeout(url):
     return url.startswith(("redis://", "rediss://", "sqs://", "sentinel://"))
 
 
-def get_default_celery_config(team_conf) -> dict[str, Any]:
+def _broker_transport_options(broker_url: str, conf: AirflowSDKConfigParser | 
Any) -> dict[str, Any]:
+    """
+    Parse broker_transport_options including dict options.
+
+    :param broker_url: Celery broker url
+    :param conf: ExecutorConf object
+    :return: broker_transport_options dict
+    """
+    broker_transport_options: dict[str, str | int | float | Any] = (
+        conf.getsection("celery_broker_transport_options") or {}
+    )
+    if "visibility_timeout" not in broker_transport_options:
+        if _broker_supports_visibility_timeout(broker_url):
+            broker_transport_options["visibility_timeout"] = 86400
+            log.warning(
+                "No visibility_timeout configured in 
[celery_broker_transport_options]. "
+                "Using default of 86400 seconds (24 hours). Celery tasks 
running longer than this "
+                "will be redelivered by the broker, which terminates the 
original task. "
+                "If you have long-running tasks, increase this value in your 
Airflow configuration: "
+                "[celery_broker_transport_options] visibility_timeout = 
<seconds>"
+            )
+
+    # Parse dict options
+    for option in _BROKER_TRANSPORT_DICT_OPTIONS:
+        if option in broker_transport_options:
+            try:
+                option_value = broker_transport_options[option]
+                if not isinstance(option_value, str):
+                    raise ValueError(f"broker_transport_option {option} is not 
string: {option_value}")
+                option_json = json.loads(option_value)
+                if not isinstance(option_json, dict):
+                    raise ValueError(
+                        f"broker_transport_option {option} value is not 
dictionary: {option_json}"
+                    )
+                broker_transport_options[option] = option_json
+            except Exception as exc:
+                raise ValueError(
+                    f"Broker transport option {option} value should be written 
in the correct JSON format."
+                ) from exc
+    return broker_transport_options
+
+
+def get_default_celery_config(team_conf: AirflowSDKConfigParser | Any) -> 
dict[str, Any]:
     """
     Build Celery configuration using team-aware config.
 
@@ -65,26 +129,7 @@ def get_default_celery_config(team_conf) -> dict[str, Any]:
 
     broker_url = team_conf.get("celery", "BROKER_URL", 
fallback="redis://redis:6379/0")
 
-    broker_transport_options: dict = 
team_conf.getsection("celery_broker_transport_options") or {}
-    if "visibility_timeout" not in broker_transport_options:
-        if _broker_supports_visibility_timeout(broker_url):
-            broker_transport_options["visibility_timeout"] = 86400
-            log.warning(
-                "No visibility_timeout configured in 
[celery_broker_transport_options]. "
-                "Using default of 86400 seconds (24 hours). Celery tasks 
running longer than this "
-                "will be redelivered by the broker, which terminates the 
original task. "
-                "If you have long-running tasks, increase this value in your 
Airflow configuration: "
-                "[celery_broker_transport_options] visibility_timeout = 
<seconds>"
-            )
-
-    if "sentinel_kwargs" in broker_transport_options:
-        try:
-            sentinel_kwargs = 
json.loads(broker_transport_options["sentinel_kwargs"])
-            if not isinstance(sentinel_kwargs, dict):
-                raise ValueError
-            broker_transport_options["sentinel_kwargs"] = sentinel_kwargs
-        except Exception:
-            raise AirflowException("sentinel_kwargs should be written in the 
correct dictionary format.")
+    broker_transport_options = _broker_transport_options(broker_url, team_conf)
 
     if team_conf.has_option("celery", "RESULT_BACKEND"):
         result_backend = team_conf.get_mandatory_value("celery", 
"RESULT_BACKEND")
@@ -177,15 +222,17 @@ def get_default_celery_config(team_conf) -> dict[str, 
Any]:
                     "Set SSL_MUTUAL_TLS=True if you intend to use mutual TLS."
                 )
 
+            broker_use_ssl: dict[str, str | int] = {}
+
             if broker_url and re.search(r"amqps?://", broker_url):
-                broker_use_ssl = {"cert_reqs": ssl.CERT_REQUIRED}
+                broker_use_ssl["cert_reqs"] = ssl.CERT_REQUIRED
                 if ssl_cacert:
                     broker_use_ssl["ca_certs"] = ssl_cacert
                 if ssl_mutual_tls:
                     broker_use_ssl["keyfile"] = ssl_key
                     broker_use_ssl["certfile"] = ssl_cert
             elif broker_url and re.search("rediss?://|sentinel://", 
broker_url):
-                broker_use_ssl = {"ssl_cert_reqs": ssl.CERT_REQUIRED}
+                broker_use_ssl["ssl_cert_reqs"] = ssl.CERT_REQUIRED
                 if ssl_cacert:
                     broker_use_ssl["ssl_ca_certs"] = ssl_cacert
                 if ssl_mutual_tls:
diff --git a/providers/celery/src/airflow/providers/celery/get_provider_info.py 
b/providers/celery/src/airflow/providers/celery/get_provider_info.py
index 4a79cc58821..02c71ab36ac 100644
--- a/providers/celery/src/airflow/providers/celery/get_provider_info.py
+++ b/providers/celery/src/airflow/providers/celery/get_provider_info.py
@@ -292,6 +292,48 @@ def get_provider_info():
             "celery_result_backend_transport_options": {
                 "description": "This section is for specifying options which 
can be passed to the\nunderlying celery result backend transport. This is 
particularly useful when using\nRedis Sentinel as the result backend. 
See:\nhttps://docs.celeryq.dev/en/latest/userguide/configuration.html#std:setting-result_backend_transport_options\n";,
                 "options": {
+                    "client-config": {
+                        "description": "SQS botocore client config.\n",
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": '{"connect_timeout": 5}',
+                        "default": None,
+                    },
+                    "fetch_message_attributes": {
+                        "description": "SQS fetch message attributes.\n",
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": '{"MessageSystemAttributeNames": 
["SenderId", "SentTimestamp"]}',
+                        "default": None,
+                    },
+                    "kafka_admin_config": {
+                        "description": "Kafka admin only config, see 
``kafka_common_config``.\n",
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": None,
+                        "default": None,
+                    },
+                    "kafka_common_config": {
+                        "description": "Kafka config common to producers and 
consumers,\nsee 
https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/kafka.html.\n";,
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": '{"bootstrap.servers": "broker:9094"}',
+                        "default": None,
+                    },
+                    "kafka_consumer_config": {
+                        "description": "Kafka consumer specific config, see 
``kafka_common_config``.\n",
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": None,
+                        "default": None,
+                    },
+                    "kafka_producer_config": {
+                        "description": "Kafka producer specific config, see 
``kafka_common_config``.\n",
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": None,
+                        "default": None,
+                    },
                     "master_name": {
                         "description": "The name of the Redis Sentinel primary 
node to connect to.\nRequired when using Redis Sentinel as the result 
backend.\n",
                         "version_added": None,
@@ -299,6 +341,27 @@ def get_provider_info():
                         "example": "mymaster",
                         "default": None,
                     },
+                    "predefined_exchanges": {
+                        "description": "SQS Predefined SNS topics.\n",
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": '{"exchange-1": {"arn": 
"arn:aws:sns:us-east-1:xxx:exchange-1"}}',
+                        "default": None,
+                    },
+                    "predefined_queues": {
+                        "description": "SQS Predefined Queues.",
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": '{"queue-1": {"url": 
"https://sqs.us-east-1.amazonaws.com/xxx/aaa"}}',
+                        "default": None,
+                    },
+                    "queue_tags": {
+                        "description": "SQS Queue tags to apply during queue 
creation.\n",
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": '{"Environment": "production", "Team": 
"backend"}',
+                        "default": None,
+                    },
                     "sentinel_kwargs": {
                         "description": "The sentinel_kwargs parameter allows 
passing additional options to the Sentinel client\nfor the result backend. In a 
typical scenario where Redis Sentinel is used as the\nresult backend and Redis 
servers are password-protected, the password needs to be\npassed through this 
parameter. Although its type is string, it is required to pass\na string that 
conforms to the dictionary 
format.\nSee:\nhttps://docs.celeryq.dev/en/stable/getting-started/backend [...]
                         "version_added": None,
@@ -307,6 +370,13 @@ def get_provider_info():
                         "example": '{"password": "password_for_redis_server"}',
                         "default": None,
                     },
+                    "sqs-creation-attributes": {
+                        "description": "Additional SQS queue attributes to 
apply as key/value pairs.\n",
+                        "version_added": "3.24.0",
+                        "type": "string",
+                        "example": '{"KmsMasterKeyId": "alias/aws/sqs"}',
+                        "default": None,
+                    },
                 },
             },
         },
diff --git 
a/providers/celery/tests/unit/celery/executors/test_celery_executor.py 
b/providers/celery/tests/unit/celery/executors/test_celery_executor.py
index 28b08405902..5d030d269d0 100644
--- a/providers/celery/tests/unit/celery/executors/test_celery_executor.py
+++ b/providers/celery/tests/unit/celery/executors/test_celery_executor.py
@@ -724,15 +724,147 @@ def 
test_celery_executor_with_no_recommended_result_backend(caplog):
         ) in caplog.text
 
 
-@conf_vars({("celery_broker_transport_options", "sentinel_kwargs"): 
'{"service_name": "mymaster"}'})
-def test_sentinel_kwargs_loaded_from_string():
+_dict_options_test_cases: list[tuple[str, str, dict]] = [
+    (
+        "client-config",
+        '{"connect_timeout": 5}',
+        {
+            "connect_timeout": 5,
+        },
+    ),
+    (
+        "fetch_message_attributes",
+        """
+            {
+                "MessageSystemAttributeNames": ["SenderId", "SentTimestamp"],
+                "MessageAttributeNames": ["S3MessageBodyKey"]
+            }
+        """,
+        {
+            "MessageSystemAttributeNames": ["SenderId", "SentTimestamp"],
+            "MessageAttributeNames": ["S3MessageBodyKey"],
+        },
+    ),
+    (
+        "predefined_exchanges",
+        """
+            {
+                "exchange-1": {
+                    "arn": "arn:aws:sns:us-east-1:xxx:exchange-1",
+                    "access_key_id": "a",
+                    "secret_access_key": "b"
+                },
+                "exchange-2.fifo": {
+                    "arn": "arn:aws:sns:us-east-1:xxx:exchange-2",
+                    "access_key_id": "c",
+                    "secret_access_key": "d"
+                }
+            }
+        """,
+        {
+            "exchange-1": {
+                "arn": "arn:aws:sns:us-east-1:xxx:exchange-1",
+                "access_key_id": "a",
+                "secret_access_key": "b",
+            },
+            "exchange-2.fifo": {
+                "arn": "arn:aws:sns:us-east-1:xxx:exchange-2",
+                "access_key_id": "c",
+                "secret_access_key": "d",
+            },
+        },
+    ),
+    (
+        "predefined_queues",
+        """
+        {
+            "queue-1": {
+                "url": "https://sqs.us-east-1.amazonaws.com/xxx/aaa";,
+                "access_key_id": "a",
+                "secret_access_key": "b",
+                "backoff_tasks": ["svc.tasks.tasks.task1"]
+            },
+            "queue-2.fifo": {
+                "url": "https://sqs.us-east-1.amazonaws.com/xxx/bbb.fifo";,
+                "access_key_id": "c",
+                "secret_access_key": "d"
+            }
+        }
+        """,
+        {
+            "queue-1": {
+                "url": "https://sqs.us-east-1.amazonaws.com/xxx/aaa";,
+                "access_key_id": "a",
+                "secret_access_key": "b",
+                "backoff_tasks": ["svc.tasks.tasks.task1"],
+            },
+            "queue-2.fifo": {
+                "url": "https://sqs.us-east-1.amazonaws.com/xxx/bbb.fifo";,
+                "access_key_id": "c",
+                "secret_access_key": "d",
+            },
+        },
+    ),
+    (
+        "queue_tags",
+        """
+        {
+                "Environment": "production",
+                "Team": "backend"
+        }
+        """,
+        {
+            "Environment": "production",
+            "Team": "backend",
+        },
+    ),
+    (
+        "sqs-creation-attributes",
+        """
+        {
+            "KmsMasterKeyId": "alias/aws/sqs"
+        }
+        """,
+        {
+            "KmsMasterKeyId": "alias/aws/sqs",
+        },
+    ),
+    (
+        "kafka_admin_config",
+        '{"sasl.username": "foo", "sasl.password": "bar"}',
+        {"sasl.username": "foo", "sasl.password": "bar"},
+    ),
+    ("kafka_common_config", '{"compression.type": "zstd"}', 
{"compression.type": "zstd"}),
+    (
+        "kafka_consumer_config",
+        '{"group.id": "myconsumer"}',
+        {"group.id": "myconsumer"},
+    ),
+    (
+        "kafka_producer_config",
+        '{"ssl.certificate.location": "/foo/bar"}',
+        {"ssl.certificate.location": "/foo/bar"},
+    ),
+    ("sentinel_kwargs", '{"service_name": "mymaster"}', {"service_name": 
"mymaster"}),
+]
+
+
[email protected](
+    (
+        "option",
+        "value",
+        "expected",
+    ),
+    _dict_options_test_cases,
+    ids=[t[0] for t in _dict_options_test_cases],
+)
+def test_dict_options_loaded_from_string(option, value, expected):
     import importlib
 
     # Reload celery conf to apply the new config.
-    importlib.reload(default_celery)
-    assert 
default_celery.DEFAULT_CELERY_CONFIG["broker_transport_options"]["sentinel_kwargs"]
 == {
-        "service_name": "mymaster"
-    }
+    with conf_vars({("celery_broker_transport_options", option): value}):
+        importlib.reload(default_celery)
+        assert 
default_celery.DEFAULT_CELERY_CONFIG["broker_transport_options"][option] == 
expected
 
 
 @conf_vars({("celery", "task_acks_late"): "False"})

Reply via email to