1
0
Fork 0

Retry artifact download when response stream is truncated

pull/661/head
Chris Sidi 2020-11-25 23:04:07 -05:00 committed by Yang Cao
parent ff4308098f
commit 520206f818
2 changed files with 71 additions and 6 deletions

View File

@ -124,7 +124,7 @@ describe('Download Tests', () => {
) )
const targetPath = path.join(root, 'FileA.txt') const targetPath = path.join(root, 'FileA.txt')
setupDownloadItemResponse(true, 200, fileContents) setupDownloadItemResponse(true, 200, false, fileContents)
const downloadHttpClient = new DownloadHttpClient() const downloadHttpClient = new DownloadHttpClient()
const items: DownloadItem[] = [] const items: DownloadItem[] = []
@ -147,7 +147,7 @@ describe('Download Tests', () => {
) )
const targetPath = path.join(root, 'FileB.txt') const targetPath = path.join(root, 'FileB.txt')
setupDownloadItemResponse(false, 200, fileContents) setupDownloadItemResponse(false, 200, false, fileContents)
const downloadHttpClient = new DownloadHttpClient() const downloadHttpClient = new DownloadHttpClient()
const items: DownloadItem[] = [] const items: DownloadItem[] = []
@ -171,7 +171,7 @@ describe('Download Tests', () => {
const fileContents = Buffer.from('try, try again\n', defaultEncoding) const fileContents = Buffer.from('try, try again\n', defaultEncoding)
const targetPath = path.join(root, `FileC-${statusCode}.txt`) const targetPath = path.join(root, `FileC-${statusCode}.txt`)
setupDownloadItemResponse(false, statusCode, fileContents) setupDownloadItemResponse(false, statusCode, false, fileContents)
const downloadHttpClient = new DownloadHttpClient() const downloadHttpClient = new DownloadHttpClient()
const items: DownloadItem[] = [] const items: DownloadItem[] = []
@ -188,6 +188,52 @@ describe('Download Tests', () => {
} }
}) })
it('Test retry on truncated response with gzip', async () => {
const fileContents = Buffer.from(
'Sometimes gunzip fails on the first try\n',
defaultEncoding
)
const targetPath = path.join(root, 'FileD.txt')
setupDownloadItemResponse(true, 200, true, fileContents)
const downloadHttpClient = new DownloadHttpClient()
const items: DownloadItem[] = []
items.push({
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileD.txt`,
targetPath
})
await expect(
downloadHttpClient.downloadSingleArtifact(items)
).resolves.not.toThrow()
await checkDestinationFile(targetPath, fileContents)
})
it('Test retry on truncated response without gzip', async () => {
const fileContents = Buffer.from(
'You have to inspect the content-length header to know if you got everything\n',
defaultEncoding
)
const targetPath = path.join(root, 'FileE.txt')
setupDownloadItemResponse(false, 200, true, fileContents)
const downloadHttpClient = new DownloadHttpClient()
const items: DownloadItem[] = []
items.push({
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileD.txt`,
targetPath
})
await expect(
downloadHttpClient.downloadSingleArtifact(items)
).resolves.not.toThrow()
await checkDestinationFile(targetPath, fileContents)
})
/** /**
* Helper used to setup mocking for the HttpClient * Helper used to setup mocking for the HttpClient
*/ */
@ -253,17 +299,24 @@ describe('Download Tests', () => {
function setupDownloadItemResponse( function setupDownloadItemResponse(
isGzip: boolean, isGzip: boolean,
firstHttpResponseCode: number, firstHttpResponseCode: number,
truncateFirstResponse: boolean,
fileContents: Buffer fileContents: Buffer
): void { ): void {
const spyInstance = jest const spyInstance = jest
.spyOn(HttpClient.prototype, 'get') .spyOn(HttpClient.prototype, 'get')
.mockImplementationOnce(async () => { .mockImplementationOnce(async () => {
if (firstHttpResponseCode === 200) { if (firstHttpResponseCode === 200) {
const fullResponse = await constructResponse(isGzip, fileContents)
const actualResponse = truncateFirstResponse
? fullResponse.subarray(0, 3)
: fullResponse
return { return {
message: getDownloadResponseMessage( message: getDownloadResponseMessage(
firstHttpResponseCode, firstHttpResponseCode,
isGzip, isGzip,
await constructResponse(isGzip, fileContents) fullResponse.length,
actualResponse
), ),
readBody: emptyMockReadBody readBody: emptyMockReadBody
} }
@ -272,6 +325,7 @@ describe('Download Tests', () => {
message: getDownloadResponseMessage( message: getDownloadResponseMessage(
firstHttpResponseCode, firstHttpResponseCode,
false, false,
0,
null null
), ),
readBody: emptyMockReadBody readBody: emptyMockReadBody
@ -283,11 +337,13 @@ describe('Download Tests', () => {
if (firstHttpResponseCode !== 200) { if (firstHttpResponseCode !== 200) {
spyInstance.mockImplementationOnce(async () => { spyInstance.mockImplementationOnce(async () => {
// chained response, if the HTTP GET function gets called again, return a successful response // chained response, if the HTTP GET function gets called again, return a successful response
const fullResponse = await constructResponse(isGzip, fileContents)
return { return {
message: getDownloadResponseMessage( message: getDownloadResponseMessage(
200, 200,
isGzip, isGzip,
await constructResponse(isGzip, fileContents) fullResponse.length,
fullResponse
), ),
readBody: emptyMockReadBody readBody: emptyMockReadBody
} }
@ -311,6 +367,7 @@ describe('Download Tests', () => {
function getDownloadResponseMessage( function getDownloadResponseMessage(
httpResponseCode: number, httpResponseCode: number,
isGzip: boolean, isGzip: boolean,
contentLength: number,
response: Buffer | null response: Buffer | null
): http.IncomingMessage { ): http.IncomingMessage {
let readCallCount = 0 let readCallCount = 0
@ -335,7 +392,9 @@ describe('Download Tests', () => {
}) })
mockMessage.statusCode = httpResponseCode mockMessage.statusCode = httpResponseCode
mockMessage.headers = {} mockMessage.headers = {
'content-length': contentLength.toString()
}
if (isGzip) { if (isGzip) {
mockMessage.headers['content-encoding'] = 'gzip' mockMessage.headers['content-encoding'] = 'gzip'

View File

@ -264,6 +264,12 @@ export class DownloadHttpClient {
const gunzip = zlib.createGunzip() const gunzip = zlib.createGunzip()
response.message response.message
.pipe(gunzip) .pipe(gunzip)
.on('error', error => {
core.error(
`An error has been encountered while attempting to decompress a file`
)
reject(error)
})
.pipe(destinationStream) .pipe(destinationStream)
.on('close', () => { .on('close', () => {
resolve() resolve()