From 80e86e7bc3e8e7b1f096c81b33b73e14a320c117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 4 Jul 2026 09:39:26 +0200 Subject: [PATCH] fix: make artifact inventory consumption deterministic on download Artifact downloads consumed the inventory entry only from the source stream's 'end' / response 'finish' handlers. But the response carries an explicit content-length, so a client's fetch completes as soon as that many body bytes arrive - which happens right after the final chunk is written to the socket, while the server's read stream still needs one more threadpool read to detect EOF and emit 'end'. Under CPU contention (e.g. the instrumented Coverage CI job) that threadpool completion can land after the client's follow-up GET /artifacts, which then still lists the consumed artifact. Consume the entry as soon as the stream has emitted content-length bytes, before pipe() writes the final chunk to the client. Any client that observed a complete download is now guaranteed to observe the entry as consumed. The 'end'/'finish'/'close' handlers stay as backstops (idempotent via didCleanupArtifact). --- src/daemon/downloadable-artifact-http.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/daemon/downloadable-artifact-http.ts b/src/daemon/downloadable-artifact-http.ts index d5254732a8..979ee3cc47 100644 --- a/src/daemon/downloadable-artifact-http.ts +++ b/src/daemon/downloadable-artifact-http.ts @@ -113,6 +113,14 @@ async function handleArtifactDownload( didCleanupArtifact = true; cleanupDownloadableArtifact(artifactId); }; + // Consume before the final chunk is written to the client: fetch treats a + // download as complete once content-length bytes arrive, which can happen + // before this stream emits 'end' (that needs one more threadpool read). + let bytesSent = 0; + stream.on('data', (chunk: string | Buffer) => { + bytesSent += chunk.length; + if (bytesSent >= artifact.sizeBytes) cleanupCompletedDownload(); + }); stream.on('end', cleanupCompletedDownload); res.on('finish', cleanupCompletedDownload); res.on('close', () => {