State: Draft
Discussion thread:
JIRA: AIRFLOW-2221
Currently Airflow requires dag files to be present on a file system that is accessible to the scheduler, webserver, and worker/s. An increasing number of Airflow implementations are done on distributed cloud frameworks, which adds an increased level of difficult for guaranteeing a file system that is accessible and synced amongst services. By allowing Airflow the ability to fetch dag files from a remote source outside the file system local to the service, this grant a much greater flexibility, eases implementation, and standardizes ways to sync remote sources of dags with Airflow.
The following outline is based upon the work already done in the PR: https://github.com/apache/incubator-airflow/pull/3138
Currently Airflow assumes valid DAGs are any Dag object that it finds in the python files in the $AIRFLOW_HOME/dags directory, the system is not the most efficient or the most desirable.
The Dag Manifest would be composed of manifest entries, an entry would be defined as follows:
entry: uri: where dag can be found conn_id: connection id to use to interact with remote location |
The manifest would be generated by a callable supplied in the config that when called would generate a list of entries i.e
[core] # callable to fetch dag manifest list dag_list = my_config.get_dag_list |
To maintain backwards compatibility to the default for dag_list wold be to simply crawl the dags folder and then produce the manifest that way
DAG locations will be given via URI, i.e. s3://my-bucket/dag1.zip, local:////dags/day1.zip
DAG URI is also DAG version.
Moving DAGs to a remote location will introduce network overhead so we should cache DAGs and avoid unnecessary fetch. We should only re-fetch a DAG when the URI in the DAG manifest is changed.
In order to avoid making a remote fetch every time the dag needs to be run it will be best to keep a local cache of dag files for individual Airflow services to use
cached_dags_path/<DAG_ID>/<DAG_URI>
The main part of the code base that we will need to change is in DagBag. In collect_dags, we will go through each entries in defined in the DAG manifest and download the DAG files if cache is invalid. In download_dag_file_and_add_to_cache, we will use different fetching implementation for different kind of uri, e.g. s3 or git.
class DagBag(): def collect_dags(): for entry in get_dag_manifest_entries(): dag_cache_path = self.get_cache_apth(entry.dag_id, entry.uri) if os.direxists(dag_cache_path): # we have the latest cache of DAG continue else: download_dag_file_and_add_to_cache(dag_id, entry.uri) self.process_file(cache_path) def download_dag_file_and_add_to_cache(dag_id, uri, dag_cache_path): """ Download DAG files from remote location to the local dag_cache_path. """ uri_type = get_uri_type(uri) self.get_dag_fetcher(uri_type).fetch_and_cache_dag(dag_id, uri, dag_cache_path) def get_cache_path(dag_id, uri): return cached_dags_path + "/" + dag_id + "/" + uri cass DagFetcher(): def fetch_and_cache_dag(dag_id, uri, conn_id, dag_cache_path): raise NotImplementedError() |
Currently the scheduler checks what dags are on disk by calling list_py_file_paths this will need to be changed to instead look at the manifest as we can no longer crawl the file system, and instead crawl manifest entries
If we implement versioning of dags it will require a number of changes to the current scheduler. The biggest issue comes from how the scheduler currently propagates the dag object to it's various function calls for task scheduling. As is the scheduler loads in the dag objects that are found in the filesystem, and these passed along to the resulting functions. In order to implement versions we would need to associate a certain dag version/uri to a DagRun, when previous DagRuns are fetched we'll need to check if they were for an earlier dag version/uri and fetch that version/uri if necessary.
Here is the exact part of _process_task_instances where this check would need to happen
def _process_task_instances(self, dag, queue, session=None):
"""
This method schedules the tasks for a single DAG by looking at the
active DAG runs and adding task instances that should run to the
queue.
"""
# update the state of the previously active dag runs
dag_runs = DagRun.find(dag_id=dag.dag_id, state=State.RUNNING, session=session)
active_dag_runs = []
for run in dag_runs:
self.log.info("Examining DAG run %s", run)
# don't consider runs that are executed in the future
if run.execution_date > timezone.utcnow():
self.log.error(
"Execution date is in future: %s",
run.execution_date
)
continue
if len(active_dag_runs) >= dag.max_active_runs:
self.log.info("Active dag runs > max_active_run.")
continue
# skip backfill dagruns for now as long as they are not really scheduled
if run.is_backfill:
continue
# todo: run.dag is transient but needs to be set
run.dag = dag |
In this case the dag object will be the object loaded in from the current version listed in the manifest, this last line here should check to pin run.dag to be equal to the dag version of the DagRun
The main thing we'll have to make sure stays consistent about the dag model is the the fileloc attribute points to a file location on disk accessible to the webserver, one way to do this is to set fileloc to be the local cache location, and that when getting a dag from the DagBag we ensure the cache file is available on disk