|
This is part of AIP-1, which aims to run Airflow in multi-tenant way. The way to achieve that is splitting the Airflow components into "trusted" and "untrusted" , which allows to put security boundaries between them. "Untrusted" components could then executed in DBIsolation mode, which disables direct Database access, making it possible only through Airflow Database API (separate AIP).
DagProcessor is currently part of Scheduler. It works with user code, it is considered "untrusted", while SchedulerJob is "trusted". With changes below it will be possible to start DagProcessor and SchedulerJob as independent processes running on different hosts.
Finding zombies is currently run in the DAG file processing loop, however it feels to have more in common with Scheduler Job than processing files.
I propose to move the code of _find_zombies method to SchedulerJob class and run it there on a configured interval (next to adopt_or_reset_orphaned_tasks or emit_metrics).
The interval is a new configuration option [scheduler]zombie_detection_interval with default to 30 seconds - currently it's 10 seconds, which sounds too often.
I propose to add a new configuration option [scheduler]standalone_dag_processor defaulted to False.
When this option is "True", the SchedulerJob doesn't start the DagFileProcessortAgent on startup. It's up to the user to run it in an independent process.
DAG processor can be executed in standalone mode with airflow dag-processor command, accepting following inputs:
On startup it executes the DagFileProcessorManager process.
Callbacks allow you to execute additional functions on dag/task success or failure. Currently they are triggered from both Scheduler Job (e.g. DagRun timeout) and Dag Processing (Zombie detection).
Callbacks are executed by Dag Processor, which makes sense as the code is defined in dag file (untrusted). However, to allow Scheduler Job to trigger callback, in case DagProcessor is running as a standalone process, the information about callback must be passed to DagProcesor.
I proposed to introduce a new Table CallbackRequest for storing the callbacks, with following fields:
id (int) - uniquely identifies the callbackcreated_at (datetime) - Time when the callback was addedpriority_weight (int) - higher values = higher prioritymessage(string) - additional Message that can be used for loggingcallback_type (enum) - TaskCallback, DagCallback or SlaCallbackcallback_data (JSON) - serialized metadata of the callbackprocessor_subdir (string) - contains the information about the dag directory(required for "multiple dags directory" described belowCallback_data allow to construct the callback object: TaskCallbackRequest, DagCallbackRequest, SlaCallbackRequest.
Scheduler-Process(via the Executor) instead of sending the callbacks to the queue adds them to the database.
Dag Processor during the execution loop fetches max_callback_per_loop=XX (higher callback_priority first), deletes them from the database, builds proper CallbackRequests objects and adds them to the "_callback_to_execute" list and processes them the same way as it does now. Each Dag Processor only executes callbacks from a directory it reads DAGs from(based on the dag_directory column).
Airflow still uses the queue in [scheduler]standalone_dag_processor=Falsemode.
Code for handling the internal queue of callbacks (currently in` DagFileProcessorManager._callback_to_execute`) is moved to airflow/callbacks/ with BaseCallbackSink and its two implementations PipeCallbackSink (sends callbacks to DagProcessor using current existing Pipe) and DatabaseCallbackSink (adds callbacks to Database).
DagProcessor in its loop calls additional method fetch_callbacks (in [scheduler]standalone_dag_processor=True mode) which fetches callbacks from database and adds them to the internal queue.
To make the callbacks handling more extendable and future-proof, new callbacks are sent for execution (both legacy and standalone dag processor mode) using a new method on BaseExecutor: send_callback_to_execute.This method is implemented in BaseExecutor by calling BaseCallbackSink , which is using, according to [scheduler]standalone_dag_processor=True mode, either PipeCallbackSink or DatabaseCallbackSink .
This allows to override this behavior in sub-Executors.
Callbacks could also be executed by workers, as they may be considered as part of "task execution" and it feels natural to execute task-related callbacks there, however there are some problems with this, especially when using KubernetesExecutor :
This change allows multiple dag directories. Each Dag Processor component parsed files from different directories, passed with --subdir/-S parameter. It allows e.g. multiple teams to share the same Airflow installation, putting dags in different directories with different sets of permissions.
To make it possible I propose to extend SerializedDagModel and DagModel tables with new column:
It contains the information about the dag directory f. During DAG file parsing, the processor sets this field value to the value of --subdir
When removing deleted dags, only dags with dag_directory equal to dag processor's --subdir value are removed/marked as inactive.
Additional cleanup of DagModel/SerializedDagModels is required as DagProcessor may be stopped or restarted with different settings, leaving the abandoned Dags in the database. They are cleaned by SchedulerJob's configured interval(running every 60 seconds) which marks as inactive all DAGs that were not updated by DagProcessor within the last 1 hour.
There is also a problem with two different dag files with the same dag_id in different directories. As a solution I propose to use Airflow DagPolicy for this purpose - users will be able to define a policy that checks if Dag ID meets the conditions, eg.
def dag_policy(dag: DAG):
subdir = FileProcessorManager.get_current_subdir()
if not dag.dag_id.startswith(subdir):
raise AirflowClusterPolicyViolation(
f"DAG's id {dag.dag_id} from file path: {dag.fileloc} does not start with '{subdir}.' even if comes from {subdir} subdirectory of DAG folder."
) |
Where FileProcessorManager.get_current_subdir is a new method returning current subdir parameter of Dag-Processor.
Below you can see the possible deployment options of Airflow installation after these changes are implemented.
The Dag Processor still runs as part of Scheduler. No changes for the end-user.
Dag Processor runs as a standalone component. Users need to start a scheduler job (airflow scheduler) and Dag Processor (airflow dag-processor) independently.
Multiple Dag Processors - each parsing dags from a different directory. Users need to start a scheduler job (airflow scheduler) and each Dag Processor (airflow dag-processor --subdir /dag-X). This allows to set up different subdirectories for dags for different teams which are parsed independently and in isolation.
What problem does it solve?
Allows to run multiple Dag Processors as independent components on separate hosts parsing DAGs from distinct locations.
This is needed to run DagProcessor in "DBIsolation" mode (separate AIP), without direct access to the database while Scheduler Job still has this access.
Only those that set [scheduler]standalone_dag_processor=True
DB upgraded required to use the new mode.
DagProcessor and SchedulerJob are executed independently on different hosts. it is possible to run multiple DagProcessors parsing DAGs from different directories.
|
This is part of AIP-1, which aims to run Airflow in multi-tenant way. The way to achieve that is splitting the Airflow components into "trusted" and "untrusted" , which allows to put security boundaries between them. "Untrusted" components could then executed in DBIsolation mode, which disables direct Database access, making it possible only through Airflow Database API (separate AIP).
DagProcessor is currently part of Scheduler. It works with user code, it is considered "untrusted", while SchedulerJob is "trusted". With changes below it will be possible to start DagProcessor and SchedulerJob as independent processes running on different hosts.
Finding zombies is currently run in the DAG file processing loop, however it feels to have more in common with Scheduler Job than processing files.
I propose to move the code of _find_zombies method to SchedulerJob class and run it there on a configured interval (next to adopt_or_reset_orphaned_tasks or emit_metrics).
The interval is a new configuration option [scheduler]zombie_detection_interval with default to 30 seconds - currently it's 10 seconds, which sounds too often.
I propose to add a new configuration option [scheduler]standalone_dag_processor defaulted to False.
When this option is "True", the SchedulerJob doesn't start the DagFileProcessortAgent on startup. It's up to the user to run it in an independent process.
DAG processor can be executed in standalone mode with airflow dag-processor command, accepting following inputs:
On startup it executes the DagFileProcessorManager process.
Callbacks allow you to execute additional functions on dag/task success or failure. Currently they are triggered from both Scheduler Job (e.g. DagRun timeout) and Dag Processing (Zombie detection).
Callbacks are executed by Dag Processor, which makes sense as the code is defined in dag file (untrusted). However, to allow Scheduler Job to trigger callback, in case DagProcessor is running as a standalone process, the information about callback must be passed to DagProcesor.
I proposed to introduce a new Table CallbackRequest for storing the callbacks, with following fields:
id (int) - uniquely identifies the callbackcreated_at (datetime) - Time when the callback was addedpriority_weight (int) - higher values = higher prioritymessage(string) - additional Message that can be used for loggingcallback_type (enum) - TaskCallback, DagCallback or SlaCallbackcallback_data (JSON) - serialized metadata of the callbackdag_directory (string) - contains the information about the dag directory(required for "multiple dags directory" described belowCallback_data allow to construct the callback object: TaskCallbackRequest, DagCallbackRequest, SlaCallbackRequest.
Scheduler-Process(via the Executor) instead of sending the callbacks to the queue adds them to the database.
Dag Processor during the execution loop fetches max_callback_per_loop=XX (higher callback_priority first), deletes them from the database, builds proper CallbackRequests objects and adds them to the "_callback_to_execute" list and processes them the same way as it does now. Each Dag Processor only executes callbacks from a directory it reads DAGs from(based on the dag_directory column).
Airflow still uses the queue in [scheduler]standalone_dag_processor=Falsemode.
Code for handling the internal queue of callbacks (currently in` DagFileProcessorManager._callback_to_execute`) is moved to airflow/callbacks/ with BaseCallbackSink and its two implementations PipeCallbackSink (sends callbacks to DagProcessor using current existing Pipe) and DatabaseCallbackSink (adds callbacks to Database).
DagProcessor in its loop calls additional method fetch_callbacks (in [scheduler]standalone_dag_processor=True mode) which fetches callbacks from database and adds them to the internal queue.
To make the callbacks handling more extendable and future-proof, new callbacks are sent for execution (both legacy and standalone dag processor mode) using a new method on BaseExecutor: send_callback_to_execute.This method is implemented in BaseExecutor by calling BaseCallbackSink , which is using, according to [scheduler]standalone_dag_processor=True mode, either PipeCallbackSink or DatabaseCallbackSink .
This allows to override this behavior in sub-Executors.
Callbacks could also be executed by workers, as they may be considered as part of "task execution" and it feels natural to execute task-related callbacks there, however there are some problems with this, especially when using KubernetesExecutor :
This change allows multiple dag directories. Each Dag Processor component parsed files from different directories, passed with --subdir/-S parameter. It allows e.g. multiple teams to share the same Airflow installation, putting dags in different directories with different sets of permissions.
To make it possible I propose to extend SerializedDagModel and DagModel tables with new column:
It contains the information about the dag directory f. During DAG file parsing, the processor sets this field value to the value of --subdir
When removing deleted dags, only dags with dag_directory equal to dag processor's --subdir value are removed/marked as inactive.
Additional cleanup of DagModel/SerializedDagModels is required as DagProcessor may be stopped or restarted with different settings, leaving the abandoned Dags in the database. They are cleaned by SchedulerJob's configured interval(running every 60 seconds) which marks as inactive all DAGs that were not updated by DagProcessor within the last 1 hour.
There is also a problem with two different dag files with the same dag_id in different directories. As a solution I propose to use Airflow DagPolicy for this purpose - users will be able to define a policy that checks if Dag ID meets the conditions, eg.
def dag_policy(dag: DAG):
subdir = FileProcessorManager.get_current_subdir()
if not dag.dag_id.startswith(subdir):
raise AirflowClusterPolicyViolation(
f"DAG's id {dag.dag_id} from file path: {dag.fileloc} does not start with '{subdir}.' even if comes from {subdir} subdirectory of DAG folder."
) |
Where FileProcessorManager.get_current_subdir is a new method returning current subdir parameter of Dag-Processor.
Below you can see the possible deployment options of Airflow installation after these changes are implemented.
The Dag Processor still runs as part of Scheduler. No changes for the end-user.
Dag Processor runs as a standalone component. Users need to start a scheduler job (airflow scheduler) and Dag Processor (airflow dag-processor) independently.
Multiple Dag Processors - each parsing dags from a different directory. Users need to start a scheduler job (airflow scheduler) and each Dag Processor (airflow dag-processor --subdir /dag-X). This allows to set up different subdirectories for dags for different teams which are parsed independently and in isolation.
What problem does it solve?
Allows to run multiple Dag Processors as independent components on separate hosts parsing DAGs from distinct locations.
This is needed to run DagProcessor in "DBIsolation" mode (separate AIP), without direct access to the database while Scheduler Job still has this access.
Only those that set [scheduler]standalone_dag_processor=True
DB upgraded required to use the new mode.
DagProcessor and SchedulerJob are executed independently on different hosts. it is possible to run multiple DagProcessors parsing DAGs from different directories.