Caching codegen tasks whose inputs come from a *remote* source (git SHA / introspection)? #13124
bitttttten
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Goals
tl;dr we have a task that is cacheable, but the inputs come from a remote source.
Let's say.. an async input. In the context of this issue, I'm making a "codegen" task that can depend on a remote graphql schema file/introspection query.. or with buildbuf or protobuf, you can point your codegen to run against a "git_repo" that can change. both of these are async inputs, and so our codegen task cannot be cached as turbo doesn't know about this "remote input". If it did, this our codegen could be broken up into 2 steps: 1. fetch the remote input (i.e. git sha). 2 feet git sha into codegen task as an input - since our codegen, if run against the same git sha, produces the same output.
Goals:
Let a
codegentask (thatbuilddepends on) be a cache hit when its real input, an external/remote source (e.g. a schema service queried via introspection, or a separate git repository of definitions), has not changed without every machine having to run the slow/rate-limited generator just to discover that nothing changedAllow a small, fast "resolve the identifier" step (e.g.
git ls-remotefor the default-branch SHA, or a cheap introspection check) to run inside the Turbo graph and have its result become the cache key for the expensivecodegentask, soturbo run build"just works" and caches identically on developer machines and in CI.Have this remote-derived input compose correctly with
turbo query affectedTasks, so it invalidates codegen/build` when the remote actually changes, but does not spuriously mark unrelated packages as affected.Non-goals
Changing how Turbo hashes local file inputs.. local source files already work well.. this is only about inputs whose source of truth lives "outside the working tree" (I consider them as "remote inputs").
Prescribing the transport for the resolved identifier (file vs. env var vs. anything else).. I don't mind which mechanism, only that
codegen's cache key reflects it. I could write this to a file? I could export it as an env var?Caching the remote content itself (we're not asking Turbo to mirror a remote repo or schema).. a cheap identifier such as a git SHA or response hash is sufficient. It's up to me to make this async lookup as fast or as slow as I want to.
Background
We have a
codegentask thatbuilddepends on:{ "tasks": { "codegen": { "outputs": ["src/gen/**"] }, "build": { "dependsOn": ["codegen", "^codegen", "^build"], "outputs": ["dist/**"] } } }The catch: part of
codegen's input doesn't live in our repo. It's derived from external/remote sources that change independently of our working tree. So for example, if our GraphQL deploy with a new schema - we want to bust the codegen task (imagine we are using GraphQL Code Generator).git_repo.. it consumes a separate git repository (protobuf/schema definitions) and generates code from it. Again, if the remote schemas repo changes, we want our codegen busted.The natural cache key for "did the remote change?" is cheap to obtain.. the git SHA of the remote's default branch, or a hash of the introspection response. What we want is simply: a developer (or CI) runs
turbo run build(which triggerscodegen), andcodegenis a cache hit whenever the remote source is unchanged.. without paying the full generation cost just to learn that. For example, buildbuf codegen can take 1 to 2 mins every time, so this is a real opportunity for caching.Current alternative / prior art (our workaround): the identifier has to be known before Turbo hashes
codegen's inputs, but it's only produced by running something.. a chicken-and-egg between "task that computes the key" and "task that's keyed by it." Today we resolve the SHA / run introspection outside the Turbo graph in a pre-step (a shell script that writes a generated identifier file), then feed that file in as an input. It works, but:turbo query affectedTasks, which we use to gate which apps deploy.turbo run codegenthemselves without the setup and now we have a) a cache HIT as they may have not run the pre-step recently which leads to b) cache poisoningWe also noticed
$TURBO_JIT$(deferred input hashing, v2.9.17 / #13045) and suspect it may be the intended primitive for "compute the input, then hash it," but we're unsure of the intended shape and how it interacts with the affected graph.Proposal
Ideally we could express a fast producer step whose output becomes the input hash of the slow consumer task, while staying cacheable:
{ "tasks": { // fast: a few-hundred-ms `git ls-remote` / cheap introspection HEAD check "codegen:setup": { /* produces the remote identifier */ }, // slow/expensive: only runs when the identifier changes "codegen": { "dependsOn": ["codegen:setup"], "inputs": [ /* the identifier produced above */ ], "outputs": ["src/gen/**"] }, "build": { "dependsOn": ["codegen", "^codegen", "^build"] } } }The above syntax is "todays turbo" - I have no idea how to propose this as part of a new schema. Maybe "async": true?
{ "tasks": { "codegen:setup": { "async": true, "outputs": [".codegen-hash"] } } }. I'm not sure I'm a fan, what if the output was an env var?$TURBO_JIT$intended to cover exactly this "compute the input, then hash it" case? If so, what's the recommended shape for a producer → consumer chain, and does it compose with$TURBO_ROOT$?turbo query affectedTasksso that a remote-derived input correctly invalidates the chain when the remote changes, without marking unrelated packages as affected?Environment
build → codegendependencyAll reactions