Skip to content

Commit 9f098b5

Browse files
authored
Refine streaming cache upload handling
1 parent d416787 commit 9f098b5

3 files changed

Lines changed: 40 additions & 11 deletions

File tree

rush-plugins/rush-amazon-s3-build-cache-plugin/src/AmazonS3Client.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,16 @@ const storageRetryOptions: IStorageRetryOptions = {
7474
* Computes the SHA-256 hash of a file on disk using streaming reads.
7575
*/
7676
async function _hashFileAsync(filePath: string): Promise<string> {
77-
return await new Promise<string>((resolve, reject) => {
78-
const hash: crypto.Hash = crypto.createHash(HASH_ALGORITHM);
79-
const stream: FileSystemReadStream = FileSystem.createReadStream(filePath);
80-
stream.on('data', (chunk: string | Buffer) => hash.update(chunk));
81-
stream.on('end', () => resolve(hash.digest('hex')));
82-
stream.on('error', reject);
83-
});
77+
const hash: crypto.Hash = crypto.createHash(HASH_ALGORITHM);
78+
const stream: FileSystemReadStream = FileSystem.createReadStream(filePath);
79+
80+
// If this becomes a hotspot, we can move the hashing work to a worker thread
81+
// that reuses a preallocated buffer for the file reads.
82+
for await (const chunk of stream) {
83+
hash.update(chunk);
84+
}
85+
86+
return hash.digest('hex');
8487
}
8588
/**
8689
* A helper for reading and updating objects on Amazon S3
@@ -223,11 +226,10 @@ export class AmazonS3Client {
223226
true,
224227
contentHash
225228
);
229+
response.stream.resume();
226230
if (!response.ok) {
227-
response.stream.resume();
228231
throw new Error(`Amazon S3 responded with status code ${response.status} (${response.statusText})`);
229232
}
230-
response.stream.resume();
231233
}
232234

233235
private _writeDebugLine(...messageParts: string[]): void {

rush-plugins/rush-http-build-cache-plugin/src/HttpBuildCacheProvider.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,16 @@ export class HttpBuildCacheProvider implements ICloudBuildCacheProvider {
202202
}
203203

204204
try {
205+
const { size } = await FileSystem.getStatisticsAsync(localFilePath);
205206
const entryStream: FileSystemReadStream = FileSystem.createReadStream(localFilePath);
206207
const result: IWebClientStreamResponse | false = await this._makeHttpStreamRequestAsync({
207208
terminal,
208209
relUrl: `${this._cacheKeyPrefix}${cacheId}`,
209210
method: this._uploadMethod,
210211
body: entryStream,
212+
headers: {
213+
'Content-Length': `${size}`
214+
},
211215
warningText: 'Could not write cache entry',
212216
// maxAttempts is 1 because the file read stream is consumed after the first attempt
213217
// and cannot be replayed. Downloads use MAX_HTTP_CACHE_ATTEMPTS since each retry
@@ -353,6 +357,7 @@ export class HttpBuildCacheProvider implements ICloudBuildCacheProvider {
353357
relUrl: string;
354358
method: 'GET' | UploadMethod;
355359
body: Readable | undefined;
360+
headers?: Record<string, string>;
356361
warningText: string;
357362
maxAttempts: number;
358363
credentialOptions?: CredentialsOptions;
@@ -383,12 +388,22 @@ export class HttpBuildCacheProvider implements ICloudBuildCacheProvider {
383388
relUrl: string;
384389
method: 'GET' | UploadMethod;
385390
body: Buffer | Readable | undefined;
391+
headers?: Record<string, string>;
386392
warningText: string;
387393
maxAttempts: number;
388394
credentialOptions?: CredentialsOptions;
389395
stream: boolean;
390396
}): Promise<IWebClientResponse | IWebClientStreamResponse | false> {
391-
const { terminal, relUrl, method, body, warningText, credentialOptions, stream } = options;
397+
const {
398+
terminal,
399+
relUrl,
400+
method,
401+
body,
402+
headers: requestHeaders,
403+
warningText,
404+
credentialOptions,
405+
stream
406+
} = options;
392407
const safeCredentialOptions: CredentialsOptions = credentialOptions ?? CredentialsOptions.Optional;
393408
const credentials: string | undefined = await this._tryGetCredentialsAsync(safeCredentialOptions);
394409
const url: string = new URL(relUrl, this._url).href;
@@ -404,6 +419,12 @@ export class HttpBuildCacheProvider implements ICloudBuildCacheProvider {
404419
}
405420
}
406421

422+
if (requestHeaders) {
423+
for (const [key, value] of Object.entries(requestHeaders)) {
424+
headers[key] = value;
425+
}
426+
}
427+
407428
const bodyLengthDesc: string = Buffer.isBuffer(body) ? `${body.length} bytes` : 'unknown length';
408429

409430
terminal.writeDebugLine(`[http-build-cache] request: ${method} ${url} ${bodyLengthDesc}`);

rush-plugins/rush-http-build-cache-plugin/src/test/HttpBuildCacheProvider.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ describe('HttpBuildCacheProvider', () => {
5757
jest
5858
.spyOn(FileSystem, 'createReadStream')
5959
.mockReturnValue({ pipe: jest.fn() } as unknown as ReturnType<typeof FileSystem.createReadStream>);
60+
jest
61+
.spyOn(FileSystem, 'getStatisticsAsync')
62+
.mockResolvedValue({ size: 123 } as Awaited<ReturnType<typeof FileSystem.getStatisticsAsync>>);
6063
jest
6164
.spyOn(FileSystem, 'createWriteStreamAsync')
6265
.mockResolvedValue({} as unknown as Awaited<ReturnType<typeof FileSystem.createWriteStreamAsync>>);
@@ -413,7 +416,10 @@ Array [
413416
expect(streamFetchFn).toHaveBeenCalledWith(
414417
'https://buildcache.example.acme.com/some-key',
415418
expect.objectContaining({
416-
method: 'POST'
419+
method: 'POST',
420+
headers: expect.objectContaining({
421+
'Content-Length': '123'
422+
})
417423
})
418424
);
419425
});

0 commit comments

Comments
 (0)