Repository navigation
Conversation
When many ephemeral processes initialize the same bundle over shared storage (e.g. KubernetesExecutor worker pods mounting a volume the dag processor keeps fresh), every initialize() pays a remote fetch it doesn't need: for our repository that adds ~15s of cold-start to every single task. With refresh_on_initialize=False, initialize() reuses the tracking repo already on disk as-is and skips all network operations. The repository is still cloned when absent, explicit refresh() calls still fetch, and version-pinned initialization is unaffected. Defaults to True - no behavior change for existing users. Co-Authored-By: Claude Fable 5 <[email protected]>
…sk repo The reuse path's inner block re-raises non-git errors (bare `raise` after repo.close()), but the outer except only caught InvalidGitRepositoryError / NoSuchPathError / GitCommandError. A checkout failing with e.g. OSError / PermissionError on the shared bundle storage therefore escaped instead of falling through to full initialization, contradicting the documented "reuse it as-is, fall back if unusable" intent. Broaden the outer except to Exception so any unusable on-disk repo falls back as intended (review feedback from @Sanjays2402). Signed-off-by: kjh0623 <[email protected]>
|
Good catch — you're right, that's a real gap, not intended. The inner block catches everything and re-raises after Fixed in 08b321e by broadening the outer Happy to add a regression test that forces a non-git error on the reuse checkout and asserts the fallback, if you'd like it in this PR. |
Covers the gap fixed in 08b321e: a PermissionError raised while opening the on-disk tracking repo must fall through to full initialization rather than propagate. The test asserts the fallback actually ran by checking the bundle ends up on the newer remote commit, which the reuse path would not have picked up (offered in review to @Sanjays2402). Signed-off-by: kjh0623 <[email protected]>
"unversioned" is not in docs/spelling_wordlist.txt, so the sphinx spellcheck failed on the generated API docs for this docstring. Say "when the bundle has no pinned version" instead of adding a word to the shared wordlist for a single sentence. Signed-off-by: kjh0623 <[email protected]>
aaron-y-chen
left a comment
There was a problem hiding this comment.
Hi, thanks for the PR :)
Since the motivation mentions KubernetesExecutor worker pod cold-starts, have you had a chance to test this in a local K8s cluster or a real KubernetesExecutor setup?
|
Yeah, I've been running this on our on-prem K8s cluster for a while now — it's actually already deployed in our production Airflow and has been working without issues. In our setup the DAG processor keeps the tracking repo fresh on the shared volume, so having every worker pod re-fetch on init was pure overhead. With refresh_on_initialize=False that ~15s per-pod cold start on task startup basically goes away, since it just reuses what's already on disk. The clone-if-missing and explicit refresh() paths still behave the same, so we haven't seen any staleness problems in practice. Happy to share more details about the environment if that'd help. |
…resh-on-initialize
The reuse path replaces the refresh() that ends the unversioned initialize, but refresh() also does head.reset(..., working_tree=True) - the reuse path only checked out tracking_ref, so a working tree a previous task left dirty was served as bundle content. _initialize()'s other short-circuit, the version-pinned one, already resets for exactly this reason. reset --hard HEAD, not clean -fd: untracked files survive refresh() too, and deleting them on shared bundle storage is not this change's call to make. Also log the best-effort fallback at warning, matching the other clone/open failure fallbacks in this file. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: kjh0623 <[email protected]>
The bundles page lists the GitDagBundle kwargs, and this one was only in the docstring - where it also did not say what makes it safe. Both now spell out that it needs storage some other component refreshes, and that it is a no-op for version-pinned bundles, so it only helps deployments running with disable_bundle_versioning. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: kjh0623 <[email protected]>
The fallback test failed the first Repo() construction of the whole initialize and then asserted len(opened) > 1, which encodes how many times _initialize happens to construct a Repo. Fail the first open of repo_path instead - that is the reuse attempt itself - and assert it was reached. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: kjh0623 <[email protected]>
|
Who besides your deployment needs this? |
|
Thanks, that's fair. You're right that this only pays off in one fairly specific layout, with versioning disabled and bundle storage shared between the Dag processor and workers. I don't have other users asking for it, so a permanent Thanks for the review, and thanks @Sanjays2402 for catching the exception handling gap. |
What this adds
An opt-in
refresh_on_initializekwarg (defaultTrue, no behavior change for existing users) onGitDagBundle, for deployments where many ephemeral processes initialize the same bundle over shared storage:Today, unversioned
initialize()always ends in a remote fetch. That is the right default for a dag processor keeping the bundle fresh, but with KubernetesExecutor every worker pod initializing the bundle pays that fetch on startup — for our repository this adds ~15s of cold-start to every single task, even though the shared volume already holds a tracking repo the dag processor refreshes continuously. Shallow/sparse clones shave the cost down but every pod still pays a network round-trip.With
refresh_on_initialize=False,initialize()reuses the tracking repo already on disk as-is and skips all network operations. Scope guards:refresh()calls still fetch as usual (both repo handles are assigned in the reuse path),This generalizes what we currently run in production as a small
GitDagBundlesubclass (workers reuse the PVC-resident tracking repo, only the dag processor fetches). Related context on the worker cold-start motivation was discussed in #70003.Included
refresh_on_initializekwarg + docstringrefresh()still fetches; default behavior unchanged (second initialize fetches latest)Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code following the guidelines
{pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.