Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
b8be9e3 to
af215f6
Compare
af215f6 to
7ec0632
Compare
| // as it might be done multiple times in parallel. | ||
| // javy -> https://github.com/Shopify/cli/issues/2877 | ||
| // trampoline -> ETXTBSY race conditions during parallel Rust builds | ||
| await Promise.all([installJavy(app), installTrampolines(app)]) |
There was a problem hiding this comment.
Have you tested dev with this change? I wonder if the dev-side preload is attached to the wrong lifecycle point, meaning this likely still misses app dev startup in some cases.
Right now the new preload runs in pushUpdatesForDraftableExtensions() in packages/app/src/cli/services/dev/processes/draftable-extension.ts, but the actual initial concurrent extension builds happen in AppEventWatcher.start() via buildExtensions(...).
Two consequences:
setupDevProcesses()usessetupDevSessionProcess(...)instead ofsetupDraftableExtensionsProcess(...)whendeveloperPlatformClient.supportsDevSessionsis true, so that path appears to get no trampoline preload at all.- More generally, the watcher startup is the place that owns the initial build fan-out, so putting the preload in a side process feels racy / easy to bypass.
Would it be safer to move this into the shared watcher startup path (e.g. before AppEventWatcher.start() begins buildExtensions(...)), so all app dev modes get the same protection?
There was a problem hiding this comment.
I'm very low context on CLI architecture, but I can definitely look into how we could do this at a higher level in the flow. Would we want to move the javy preload at the same time?
There was a problem hiding this comment.
I'm also low-context on the functions side of things. Perhaps it works for Javy at this layer, it should work for Trampoline. Hopefully somebody with more Functions experience can weigh in.
There was a problem hiding this comment.
I think it makes sense to move both preloads out of the watcher. There's probably also an argument that we don't need the preload for dev contexts as these are likely to run in long lived environments. The issue with the build and bundle flows is that they would often be run in CI on fresh env where the resource has to be downloaded fresh.
| await Promise.all(downloadPromises) | ||
| } | ||
|
|
||
| export async function installTrampolines(app: AppInterface) { |
There was a problem hiding this comment.
One compatibility concern here that my 'bot found: installTrampolines() now eagerly downloads both trampoline versions, but trampolineBinary(version) only marks Windows-on-ARM support for versions >= 2.0.1.
That means on win32/arm64, a function app that only needs the v2 trampoline may have worked before (because runTrampoline() lazily selected/downloaded just v2), but could now fail up front trying to predownload the unused v1 binary.
Would it be safer to skip pre-downloading versions that are unsupported on the current platform/arch, or otherwise avoid failing the whole command on an unused trampoline version?
There was a problem hiding this comment.
Yes, that makes sense. Let me see how we check for the needed version and look into adding that logic here.
There was a problem hiding this comment.
We can't determine which trampolines we need during the preload because we don't have WASMs to introspect, but we can protect from download errors and log. This will protect us from having issues during the preload. Thanks for catching this. 👍
…loading during parallel builds
7ec0632 to
0cbea2b
Compare
|
I've opened #7443 as an alternative fix. The underlying problem IMO is If we do opt to go with this PR, I would ask we don't swallow all errors that happen with downloading but much more selective. The error messages would be misleading if someone tries building a Function for the first time and GitHub is having an outage or if they're using the v1 trampoline on Windows for ARM. |
|
Thinking about this a little more, if the Shopify CLI, or the Functions binaries, were to drop support for Windows 10, we might be able to use Prism on Windows 11 to run the Intel binaries on ARM machines. I haven't tested it though as I don't have access to a Windows 11 machine. |
|
@jeffcharles Thanks for taking a look. Yes, your approach is much better. I completely missed the |

WHY are these changes introduced?
shopify app buildfails intermittently on CI withETXTBSY("text file busy") when building apps with multiple Shopify Function extensions:The trampoline binaries (
shopify-function-trampoline-1.0.2,shopify-function-trampoline-2.0.1) are not shipped with the npm package — they are lazily downloaded from GitHub on first use bydownloadBinary(). WhenrenderConcurrentruns parallel extension builds, multiple concurrent tasks race to download the same binary. One task opens the file for writing (moveFilewith{overwrite: true}) while another tries to execute it, and the Linux kernel returnsETXTBSY.The in-process dedup map (
downloadsInProgress) prevents redundant downloads within a single event loop tick, but the race window between the download completing and the file being executed by a concurrent build is still open.This is the same class of bug that was previously fixed for Javy binaries in #2877 — which introduced
installJavy()to pre-download Javy + plugin binaries before parallel builds start. The trampoline was simply missed because it was added later.WHAT is this pull request doing?
Adds
installTrampolines(app)— a new pre-download function that mirrors the existinginstallJavy(app)pattern. It eagerly downloads both trampoline versions (v1 and v2) before the parallel build fan-out, so that when individualrunTrampoline()calls run concurrently, the binaries are already on disk anddownloadBinary()is a no-op.Key design choice: Unlike Javy (where the version depends on each extension's
@shopify/shopify_functionpackage version), the trampoline version is determined post-build from WASM imports (shopify_function_v1vsshopify_function_v2). Since there are only two possible versions and the binaries are small, we simply pre-download both for any app with function extensions.Changes:
packages/app/src/cli/services/function/build.ts— AddedinstallTrampolines(app): skips if no function extensions, otherwise downloads both V1 and V2 trampoline binaries in parallelpackages/app/src/cli/services/build.ts— CallsinstallTrampolinesalongsideinstallJavy(viaPromise.all) beforerenderConcurrentpackages/app/src/cli/services/deploy/bundle.ts— Same pre-download before parallel deploy buildspackages/app/src/cli/services/dev/processes/draftable-extension.ts— Same pre-download before dev draft updatespackages/app/src/cli/services/function/build.test.ts— Added 2 tests: verifies both versions are downloaded when function extensions exist, verifies no downloads when they don'tHow to test your changes?
pnpm vitest run packages/app/src/cli/services/function/build.test.ts(28 tests pass including 2 new)node_modules/@shopify/cli/bin/shopify app build— should succeed consistently withoutETXTBSYinstallJavyandinstallTrampolinesrun in parallel viaPromise.allChecklist
patchfor bug fixes ·minorfor new features ·majorfor breaking changes) and added a changeset withpnpm changeset add