Add streaming APIs to build cache provider interface and all cache plugins#5746
Add streaming APIs to build cache provider interface and all cache plugins#5746
Conversation
…P plugin Add optional tryGetCacheEntryStreamByIdAsync and trySetCacheEntryStreamAsync methods to ICloudBuildCacheProvider. Implement streaming in HttpBuildCacheProvider and update OperationBuildCache to prefer streaming when available. Add fetchStreamAsync to WebClient and stream write support to FileSystemBuildCacheProvider. Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/050e10a7-3cad-4da4-93e5-9941453283b9 Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/050e10a7-3cad-4da4-93e5-9941453283b9 Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/050e10a7-3cad-4da4-93e5-9941453283b9 Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
…file Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/798065bf-040f-462e-8ea9-b9ca5659196d Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
|
@copilot - add APIs to |
…rary and update consumers to use FileSystem instead of fs directly Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/5ad20d4d-c9a4-4855-bb13-8dd9e2c1350b Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Added |
|
@copilot - are there async variants of those |
No, |
|
@copilot - take a look at the other |
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/199d4b3e-1f3f-44e1-9fc6-7b4a0e027c7e Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Added |
|
@copilot - can we add stream support to the other cache plugins? |
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/6212baeb-266c-4823-94df-251c69a8f74c Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
…y streaming upload Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/6212baeb-266c-4823-94df-251c69a8f74c Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Added streaming support to both remaining cache plugins: Amazon S3 plugin (
Azure Storage plugin (
All three cache plugins (HTTP, S3, Azure) now implement both streaming methods. All existing tests pass (45 S3, 10 Azure). |
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/b57084f4-36d7-42f3-8876-7298756b887e Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
…ix WebClient private member type mismatch
|
@copilot - |
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/13978d72-ea9a-4463-a6a2-51ecfc72f3ff Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/13978d72-ea9a-4463-a6a2-51ecfc72f3ff Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Yes, this was a gap. The buffer path in Fixed in This covers the HTTP plugin and S3 plugin since both use WebClient. The Azure plugin is not affected — it uses the Azure SDK directly ( |
iclanton
left a comment
There was a problem hiding this comment.
Thanks for the streaming build cache work — the interface design with optional streaming methods and graceful fallback is clean. A few issues I spotted during review, two of which look like functional bugs.
| @@ -264,7 +404,7 @@ export class HttpBuildCacheProvider implements ICloudBuildCacheProvider { | |||
| // required. Re-attempt the request, requiring credentials this time. | |||
There was a problem hiding this comment.
@copilot - Bug: streaming upload retry with consumed stream body
The caller (trySetCacheEntryStreamAsync) correctly sets maxAttempts: 1 to prevent the normal retry path (line 419) from re-sending a consumed stream. However, this credential fallback path doesn't check maxAttempts — it always retries. If a streaming upload is initially sent without credentials and gets a 4xx response, this code will re-invoke _makeHttpCoreRequestAsync with CredentialsOptions.Required, but options.body is the already-consumed stream from the first attempt. The second request would send an empty/finished stream body.
Suggestion: skip this credential fallback when the body is a stream (e.g. check !!body && typeof (body as Readable).pipe === 'function'), or gate it on maxAttempts > 1. This would mean streaming uploads need to provide credentials upfront, which seems like a reasonable requirement.
| } = response; | ||
| if (statusCode === 301 || statusCode === 302) { | ||
| // Drain the redirect response before following | ||
| response.resume(); |
There was a problem hiding this comment.
@copilot - Bug: response.resume() is called before the redirect switch, which drains the response body even when the redirect falls through to handleResponse.
When redirect is undefined (the default — most callers don't set it) or 'manual', and the server responds with 301/302:
response.resume()is called here, starting to drain the response- Neither
'follow'nor'error'matches in the switch - Execution falls through to
handleResponse(response, ...)at line 154 - In the buffer path,
handleResponseattachesdata/endevent listeners to read the response body — butresume()already started draining it
This creates a race where response data could be lost between the resume() call and the handler attachment. The resume() call should be moved inside the case 'follow' and case 'error' branches where the response body is intentionally being discarded:
if (statusCode === 301 || statusCode === 302) {
switch (redirect) {
case 'follow': {
response.resume(); // Only drain when we're discarding this response
if (redirectUrl) {
requestFnAsync(redirectUrl, options, true).then(resolve).catch(reject);
} else {
reject(new Error(`Received status code ${statusCode} with no location header: ${url}`));
}
return;
}
case 'error':
response.resume();
reject(new Error(`Received status code ${statusCode}: ${url}`));
return;
}
}| public async trySetCacheEntryBufferAsync( | ||
| terminal: ITerminal, | ||
| cacheId: string, | ||
| entryStream: Buffer |
There was a problem hiding this comment.
@copilot - Nit: this parameter is named `entryStream` but its type is `Buffer`. Looks like a copy-paste artifact from adding the stream variant. The interface (`ICloudBuildCacheProvider`) names it `entryBuffer` — should match for consistency.
| entryStream: Buffer | |
| entryBuffer: Buffer |
(The references on lines 92 would need updating too.)
| ); | ||
| updateLocalCacheSuccess = true; | ||
| } catch (e) { | ||
| updateLocalCacheSuccess = false; |
There was a problem hiding this comment.
@copilot - The error is silently discarded here (and in the identical catch block at line 209). The warning at line 254 only says "Unable to update the local build cache" without any detail about why it failed. This makes debugging cache failures unnecessarily difficult.
Consider at least logging the error at verbose level:
```typescript
} catch (e) {
terminal.writeVerboseLine(`Failed to update local cache: ${e}`);
updateLocalCacheSuccess = false;
}
```
| this._cloudBuildCacheProvider.trySetCacheEntryStreamAsync | ||
| ) { | ||
| // Use streaming upload to avoid loading the entire cache entry into memory | ||
| const entryStream: FileSystemReadStream = FileSystem.createReadStream(localCacheEntryPath); |
There was a problem hiding this comment.
@copilot - Minor: if `trySetCacheEntryStreamAsync` rejects, this `FileSystemReadStream` is never explicitly destroyed, leaking the file handle until GC. Since `setCloudCacheEntryPromise` isn't awaited until line 371, the cleanup is also deferred. Consider wrapping in a try/catch that calls `entryStream.destroy()` on failure, or attaching an error handler.
| headers: headers, | ||
| body: body, | ||
| redirect: 'follow', | ||
| timeoutMs: 0 // Use the default timeout |
There was a problem hiding this comment.
@copilot - Nit: the comment says "Use the default timeout" but passing `0` explicitly overrides the `WebClient._buildRequestOptions` default of 15 seconds and sets the Node.js socket timeout to 0 (no timeout). For streaming transfers of large cache entries, no timeout is probably intentional — but the comment should say that rather than claiming it uses the default.
…ype compatibility Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/d8f5e137-3cb0-4ed9-8391-50d7a33d804e Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
…error logging, stream cleanup, and nits Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/d0190bf7-a346-4a71-93bf-d5375d98b552 Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/86050f65-dd6c-45f4-ac41-95fdb860c053 Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.