Skip to content

Commit 9224860

Browse files
committed
Fix concurrency issue in downloadBinary
1 parent 2b75d58 commit 9224860

File tree

1 file changed

+40
-0
lines changed

1 file changed

+40
-0
lines changed

packages/app/src/cli/services/function/binaries.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,52 @@ export function trampolineBinary() {
203203
return _trampoline
204204
}
205205

206+
const downloadsInProgress = new Map<string, Promise<void>>()
207+
206208
export async function downloadBinary(bin: DownloadableBinary) {
207209
const isDownloaded = await fileExists(bin.path)
208210
if (isDownloaded) {
209211
return
210212
}
211213

214+
// Downloads cannot run concurrently with `exec` since the `moveFile`
215+
// operation is not atomic. It will delete the destination file if it exists
216+
// which will cause `exec` to break if it tries to execute the file before
217+
// the source file has been moved.
218+
// We prevent downloads from happening concurrently with `exec` by enforcing
219+
// that only one download happens for an executable. If we get here, we check
220+
// if a download is in progress, wait for it to finish, and return without
221+
// downloading again. If it's not in progress, then we start the download.
222+
const downloadPromise = downloadsInProgress.get(bin.path)
223+
if (downloadPromise) {
224+
await downloadPromise
225+
// Return now since we can assume it's downloaded. If it's not, an exception should've
226+
// been thrown before which will cause the user-level operation to fail anyway.
227+
return
228+
}
229+
230+
// Do not perform any `await`s until we've called `downloadsInProgress.set`.
231+
// Calling `await` before that can cause a different JS task to become active
232+
// and start a concurrent download for this binary.
233+
234+
// My mental model is `performDownload` without the `await` will run
235+
// synchronously until the first `await` in the function and then
236+
// immediately return the promise which we then immediately store. Since JS
237+
// doesn't have preemptive concurrency, we should be able to safely assume a
238+
// different task in the task queue will not run in between starting
239+
// `downloadFn` and the `set` operation on the following line.
240+
const downloadOp = performDownload(bin)
241+
downloadsInProgress.set(bin.path, downloadOp)
242+
// Ensure we clean the entry if there's a failure.
243+
try {
244+
// Wait for the download to finish
245+
await downloadOp
246+
} finally {
247+
downloadsInProgress.delete(bin.path)
248+
}
249+
}
250+
251+
async function performDownload(bin: DownloadableBinary) {
212252
const url = bin.downloadUrl(process.platform, process.arch)
213253
outputDebug(`Downloading ${bin.name} ${bin.version} from ${url} to ${bin.path}`)
214254
const dir = dirname(bin.path)

0 commit comments

Comments
 (0)