mirror of https://github.com/actions/toolkit
Merge pull request #3 from WarpBuilds/hotfix-gcs-backup-download
adds backup download method for streaming cachepull/1935/head
commit
cea490e16b
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "github-actions.warp-cache",
|
"name": "github-actions.warp-cache",
|
||||||
"version": "1.1.3",
|
"version": "1.1.11",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"description": "Github action to use WarpBuild's in-house cache offering",
|
"description": "Github action to use WarpBuild's in-house cache offering",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
@ -217,12 +217,62 @@ export async function restoreCache(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await extractStreamingTar(
|
try {
|
||||||
readStream,
|
await extractStreamingTar(
|
||||||
archivePath,
|
readStream,
|
||||||
compressionMethod,
|
archivePath,
|
||||||
downloadCommandPipe
|
compressionMethod,
|
||||||
)
|
downloadCommandPipe
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`Failed to download cache: ${error}`)
|
||||||
|
core.info(
|
||||||
|
`Streaming download failed. Likely a cloud provider issue. Retrying with multipart download`
|
||||||
|
)
|
||||||
|
// Wait 1 second
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
// Try to download the cache using the non-streaming method
|
||||||
|
try {
|
||||||
|
await cacheHttpClient.downloadCache(
|
||||||
|
cacheEntry.provider,
|
||||||
|
archiveLocation,
|
||||||
|
archivePath,
|
||||||
|
cacheEntry.gcs?.short_lived_token?.access_token ?? ''
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`Failed to download cache: ${error}`)
|
||||||
|
core.info(
|
||||||
|
`Multipart download failed. Likely a cloud provider issue. Retrying with basic download`
|
||||||
|
)
|
||||||
|
// Wait 1 second
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
// Try to download the cache using the basic method
|
||||||
|
try {
|
||||||
|
await cacheHttpClient.downloadCacheSingleThread(
|
||||||
|
cacheEntry.provider,
|
||||||
|
archiveLocation,
|
||||||
|
archivePath,
|
||||||
|
cacheEntry.gcs?.short_lived_token?.access_token ?? ''
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
core.info('Cache Miss. Failed to download cache.')
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (core.isDebug()) {
|
||||||
|
await listTar(archivePath, compressionMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||||
|
core.info(
|
||||||
|
`Cache Size: ~${Math.round(
|
||||||
|
archiveFileSize / (1024 * 1024)
|
||||||
|
)} MB (${archiveFileSize} B)`
|
||||||
|
)
|
||||||
|
|
||||||
|
await extractTar(archivePath, compressionMethod)
|
||||||
|
}
|
||||||
core.info('Cache restored successfully')
|
core.info('Cache restored successfully')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
InternalS3CompletedPart
|
InternalS3CompletedPart
|
||||||
} from './contracts'
|
} from './contracts'
|
||||||
import {
|
import {
|
||||||
|
downloadCacheGCP,
|
||||||
downloadCacheMultiConnection,
|
downloadCacheMultiConnection,
|
||||||
downloadCacheMultipartGCP,
|
downloadCacheMultipartGCP,
|
||||||
downloadCacheStreamingGCP
|
downloadCacheStreamingGCP
|
||||||
|
@ -230,6 +231,37 @@ export async function downloadCache(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadCacheSingleThread(
|
||||||
|
provider: string,
|
||||||
|
archiveLocation: string,
|
||||||
|
archivePath: string,
|
||||||
|
gcsToken?: string
|
||||||
|
): Promise<void> {
|
||||||
|
switch (provider) {
|
||||||
|
case 's3':
|
||||||
|
break
|
||||||
|
case 'gcs': {
|
||||||
|
if (!gcsToken) {
|
||||||
|
throw new Error(
|
||||||
|
'Unable to download cache from GCS. GCP token is not provided.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauth2Client = new OAuth2Client()
|
||||||
|
oauth2Client.setCredentials({access_token: gcsToken})
|
||||||
|
const storage = new Storage({
|
||||||
|
authClient: oauth2Client,
|
||||||
|
retryOptions: {
|
||||||
|
autoRetry: false,
|
||||||
|
maxRetries: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await downloadCacheGCP(storage, archiveLocation, archivePath)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function downloadCacheStreaming(
|
export function downloadCacheStreaming(
|
||||||
provider: string,
|
provider: string,
|
||||||
archiveLocation: string,
|
archiveLocation: string,
|
||||||
|
|
|
@ -313,11 +313,48 @@ export async function downloadCacheMultipartGCP(
|
||||||
await transferManager.downloadFileInChunks(objectName, {
|
await transferManager.downloadFileInChunks(objectName, {
|
||||||
destination: archivePath,
|
destination: archivePath,
|
||||||
noReturnData: true,
|
noReturnData: true,
|
||||||
chunkSizeBytes: 1024 * 1024 * 8
|
validation: 'crc32c'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.debug(`Failed to download cache: ${error}`)
|
core.debug(`Failed to download cache: ${error}`)
|
||||||
core.error(`Failed to download cache.`)
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadCacheGCP(
|
||||||
|
storage: Storage,
|
||||||
|
archiveLocation: string,
|
||||||
|
archivePath: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const timeoutDuration = 300000 // 5 minutes
|
||||||
|
|
||||||
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Download timed out')), timeoutDuration)
|
||||||
|
)
|
||||||
|
|
||||||
|
const {bucketName, objectName} =
|
||||||
|
utils.retrieveGCSBucketAndObjectName(archiveLocation)
|
||||||
|
|
||||||
|
const downloadPromise = storage
|
||||||
|
.bucket(bucketName)
|
||||||
|
.file(objectName)
|
||||||
|
.download({
|
||||||
|
destination: archivePath,
|
||||||
|
validation: 'crc32c'
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race([downloadPromise, timeoutPromise])
|
||||||
|
core.debug(
|
||||||
|
`Download completed for bucket: ${bucketName}, object: ${objectName}`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`Failed to download cache: ${error}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`Failed to download cache: ${error}`)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,7 +384,6 @@ export function downloadCacheStreamingGCP(
|
||||||
return storage.bucket(bucketName).file(objectName).createReadStream()
|
return storage.bucket(bucketName).file(objectName).createReadStream()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.debug(`Failed to download cache: ${error}`)
|
core.debug(`Failed to download cache: ${error}`)
|
||||||
core.error(`Failed to download cache.`)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -441,23 +441,49 @@ export async function extractStreamingTar(
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const handleStreamError = (
|
||||||
|
stream: NodeJS.ReadableStream | NodeJS.WritableStream,
|
||||||
|
commandName: string
|
||||||
|
) => {
|
||||||
|
stream.on('error', error => {
|
||||||
|
reject(new Error(`Error in ${commandName}: ${error.message}`))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach error handlers and pipe the streams
|
||||||
|
commandPipes.forEach(commandPipe => {
|
||||||
|
handleStreamError(commandPipe.stdin, commandPipe.spawnfile)
|
||||||
|
handleStreamError(commandPipe.stdout, commandPipe.spawnfile)
|
||||||
|
handleStreamError(commandPipe.stderr, commandPipe.spawnfile)
|
||||||
|
|
||||||
|
commandPipe.stderr.on('data', data => {
|
||||||
|
reject(
|
||||||
|
new Error(`Error in ${commandPipe.spawnfile}: ${data.toString()}`)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
stream.pipe(commandPipes[0].stdin)
|
stream.pipe(commandPipes[0].stdin).on('error', error => {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Error piping to ${commandPipes[0].spawnfile}: ${error.message}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
for (let i = 0; i < commandPipes.length - 1; i++) {
|
for (let i = 0; i < commandPipes.length - 1; i++) {
|
||||||
commandPipes[i].stdout.pipe(commandPipes[i + 1].stdin)
|
commandPipes[i].stdout
|
||||||
|
.pipe(commandPipes[i + 1].stdin)
|
||||||
commandPipes[i].stderr.on('data', data => {
|
.on('error', error => {
|
||||||
reject(
|
reject(
|
||||||
new Error(`Error in ${commandPipes[i].spawnfile}: ${data.toString()}`)
|
new Error(
|
||||||
)
|
`Error piping between ${commandPipes[i].spawnfile} and ${
|
||||||
})
|
commandPipes[i + 1].spawnfile
|
||||||
|
}: ${error.message}`
|
||||||
commandPipes[i].on('error', error => {
|
)
|
||||||
reject(
|
)
|
||||||
new Error(`Error in ${commandPipes[i].spawnfile}: ${error.message}`)
|
})
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastCommand = commandPipes[commandPipes.length - 1]
|
const lastCommand = commandPipes[commandPipes.length - 1]
|
||||||
|
@ -472,6 +498,9 @@ export async function extractStreamingTar(
|
||||||
reject(new Error(`Last command exited with code ${code}`))
|
reject(new Error(`Last command exited with code ${code}`))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
lastCommand.on('error', error => {
|
||||||
|
reject(new Error(`Error in ${lastCommand.spawnfile}: ${error.message}`))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue