This repository has been archived by the owner on Apr 29, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #109 from applandinc/feat/upload-retry
feat: Retry uploads
- Loading branch information
Showing
15 changed files
with
523 additions
and
224 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
Command = "node ./built/cli.js upload -v -d ../../land-of-apps/sample_app_6th_ed --report-file ../../land-of-apps/sample_app_6th_ed/appland-findings.json --app scanner-demo/sample_app_6th_ed" | ||
Times = (ARGV[0] || 3).to_i | ||
|
||
threads = ([Command] * Times).map do |cmd| | ||
Thread.new { | ||
system cmd | ||
} | ||
end | ||
|
||
threads.map(&:join) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { queue } from 'async'; | ||
import { readFile } from 'fs/promises'; | ||
import { join } from 'path'; | ||
|
||
import { AppMap as AppMapStruct } from '@appland/models'; | ||
|
||
import { verbose } from '../rules/lib/util'; | ||
import { ScanResults } from '../report/scanResults'; | ||
import { | ||
create as createAppMap, | ||
CreateOptions as CreateAppMapOptions, | ||
UploadAppMapResponse, | ||
} from '../integration/appland/appMap/create'; | ||
import { create as createMapset } from '../integration/appland/mapset/create'; | ||
import { | ||
create as createScannerJob, | ||
UploadResponse, | ||
} from '../integration/appland/scannerJob/create'; | ||
import { RetryOptions } from '../integration/appland/retryOptions'; | ||
|
||
export default async function create( | ||
scanResults: ScanResults, | ||
appId: string, | ||
mergeKey?: string, | ||
options: RetryOptions = {} | ||
): Promise<UploadResponse> { | ||
if (verbose()) console.log(`Uploading AppMaps and findings to application '${appId}'`); | ||
|
||
const { findings } = scanResults; | ||
|
||
const relevantFilePaths = [ | ||
...new Set(findings.filter((f) => f.appMapFile).map((f) => f.appMapFile)), | ||
] as string[]; | ||
|
||
const appMapUUIDByFileName: Record<string, string> = {}; | ||
const branchCount: Record<string, number> = {}; | ||
const commitCount: Record<string, number> = {}; | ||
|
||
const createAppMapOptions = { | ||
app: appId, | ||
} as CreateAppMapOptions; | ||
|
||
const q = queue((filePath: string, callback) => { | ||
if (verbose()) console.log(`Uploading AppMap ${filePath}`); | ||
|
||
readFile(filePath) | ||
.then((buffer: Buffer) => { | ||
const appMapStruct = JSON.parse(buffer.toString()) as AppMapStruct; | ||
const metadata = appMapStruct.metadata; | ||
const branch = appMapStruct.metadata.git?.branch; | ||
const commit = appMapStruct.metadata.git?.commit; | ||
if (branch) { | ||
branchCount[branch] ||= 1; | ||
branchCount[branch] += 1; | ||
} | ||
if (commit) { | ||
commitCount[commit] ||= 1; | ||
commitCount[commit] += 1; | ||
} | ||
|
||
return createAppMap(buffer, Object.assign(options, { ...createAppMapOptions, metadata })); | ||
}) | ||
.then((appMap: UploadAppMapResponse) => { | ||
if (appMap) { | ||
appMapUUIDByFileName[filePath] = appMap.uuid; | ||
} | ||
}) | ||
.then(() => callback()) | ||
.catch(callback); | ||
}, 3); | ||
q.error((err, filePath: string) => { | ||
console.error(`An error occurred uploading ${filePath}: ${err}`); | ||
}); | ||
if (verbose()) console.log(`Uploading ${relevantFilePaths.length} AppMaps`); | ||
q.push(relevantFilePaths); | ||
await q.drain(); | ||
|
||
const mostFrequent = (counts: Record<string, number>): string | undefined => { | ||
if (Object.keys(counts).length === 0) return; | ||
|
||
const maxCount = Object.values(counts).reduce((max, count) => Math.max(max, count), 0); | ||
return Object.entries(counts).find((e) => e[1] === maxCount)![0]; | ||
}; | ||
|
||
const branch = mostFrequent(branchCount); | ||
const commit = mostFrequent(commitCount); | ||
const mapset = await createMapset( | ||
appId, | ||
Object.values(appMapUUIDByFileName), | ||
Object.assign(options, { | ||
branch, | ||
commit, | ||
}) | ||
); | ||
|
||
console.warn('Uploading findings'); | ||
|
||
return createScannerJob(scanResults, mapset.id, appMapUUIDByFileName, { mergeKey }, options); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { IncomingMessage } from 'http'; | ||
import { RetryOptions } from './retryOptions'; | ||
import { | ||
RejectFunction, | ||
RetryHandler, | ||
ResolveFunction, | ||
} from '@appland/client/dist/src/retryHandler'; | ||
import { verbose } from '../../rules/lib/util'; | ||
|
||
const RetryDelay = 500; | ||
const MaxRetries = 3; | ||
|
||
export default function retry( | ||
description: string, | ||
retryOptions: RetryOptions, | ||
retryFn: () => Promise<IncomingMessage> | ||
): RetryHandler { | ||
const maxRetries = retryOptions.maxRetries ?? MaxRetries; | ||
const retryDelay = retryOptions.retryDelay ?? RetryDelay; | ||
|
||
let retryCount = 0; | ||
|
||
function computeDelay(): number { | ||
return retryDelay * Math.pow(2, retryCount - 1); | ||
} | ||
|
||
return (resolve: ResolveFunction, reject: RejectFunction): void => { | ||
retryCount += 1; | ||
if (retryCount > maxRetries) { | ||
reject(new Error(`${description} failed: Max retries exceeded.`)); | ||
} | ||
if (verbose()) { | ||
console.log(`Retrying ${description} in ${computeDelay()}ms`); | ||
} | ||
setTimeout(() => retryFn().then(resolve).catch(reject), computeDelay()); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export type RetryOptions = { | ||
maxRetries?: number; | ||
retryDelay?: number; | ||
}; |
Oops, something went wrong.