Skip to content

Commit 46a73f9

Browse files
authored
Retry API calls for reliability (#23)
* Retry API calls * Retryable Promise * Fix Last Release comparisons * Retry fix * Add timeout for variable set and retry logging * fix package.js * Fix mail sent var
1 parent 80d14d2 commit 46a73f9

File tree

18 files changed

+155
-65
lines changed

18 files changed

+155
-65
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99

1010
strategy:
1111
matrix:
12-
node-version: [8.x, 10.x, 12.x]
12+
node-version: [10.x]
1313

1414
steps:
1515
- uses: actions/checkout@v1

Tasks/emailReportTask/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ async function run(): Promise<void> {
2020
new EmailSender());
2121

2222
const mailSent = await reportManager.sendReportAsync(reportConfiguration);
23-
console.log(`##vso[task.setvariable variable=EmailReportTask.EmailSent;]${mailSent}`);
23+
console.log("Email Task processing complete. Setting EmailReportTask.EmailSent Variable value.");
24+
// Wait for 10 sec and timeout
25+
let val = await Promise.race([sleep(10000), setEmailSentVariable(mailSent)]);
26+
if(!val) {
27+
console.log("Unable to set variable value in 10 sec. Exiting task.");
28+
}
2429
}
2530
catch (err) {
2631
if (err instanceof ReportError) {
@@ -31,4 +36,13 @@ async function run(): Promise<void> {
3136
}
3237
}
3338

39+
function sleep(ms: number): Promise<boolean> {
40+
return new Promise(resolve => setTimeout(resolve, ms, false));
41+
}
42+
43+
async function setEmailSentVariable(mailSent: boolean) : Promise<boolean> {
44+
console.log(`##vso[task.setvariable variable=EmailReportTask.EmailSent;]${mailSent}`);
45+
return true;
46+
}
47+
3448
run();

Tasks/emailReportTask/model/ReleaseReport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class ReleaseReport extends Report {
9494
return true;
9595
}
9696

97-
return false;
97+
return null;
9898
}
9999

100100
public hasFailedTasks(): boolean {

Tasks/emailReportTask/providers/SendMailConditionProcessor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class SendMailConditionProcessor implements IPostProcessor {
5959
hasFailedTasks: boolean): Promise<boolean> {
6060

6161
var hasPrevGotSameFailures = report.hasPrevGotSameFailures();
62-
if (hasPrevGotSameFailures) {
62+
if (!isNullOrUndefined(hasPrevGotSameFailures) && hasPrevGotSameFailures) {
6363
return hasPrevGotSameFailures;
6464
}
6565

Tasks/emailReportTask/providers/pipeline/BuildDataProvider.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { IPipelineRestClient } from "../restclients/IPipelineRestClient";
1414
import { Build, TaskResult, TimelineRecord, IssueType } from "azure-devops-node-api/interfaces/BuildInterfaces";
1515
import { Timeline } from "azure-devops-node-api/interfaces/TaskAgentInterfaces";
1616
import { isNullOrUndefined } from "util";
17+
import { RetryablePromise } from "../restclients/RetryablePromise";
18+
import { DataProviderError } from "../../exceptions/DataProviderError";
1719

1820
export class BuildDataProvider implements IDataProvider {
1921

@@ -25,23 +27,31 @@ export class BuildDataProvider implements IDataProvider {
2527

2628
public async getReportDataAsync(pipelineConfig: PipelineConfiguration, reportDataConfiguration: ReportDataConfiguration): Promise<Report> {
2729
const report = ReportFactory.createNewReport(pipelineConfig) as BuildReport;
28-
const build = await this.pipelineRestClient.getPipelineAsync() as Build;
30+
const build = await this.getBuildAsync(pipelineConfig);
2931
if (build == null) {
3032
throw new PipelineNotFoundError(`ProjectId: ${pipelineConfig.$projectId}, ${pipelineConfig.$pipelineId}`);
3133
}
3234

33-
const timeline = await this.pipelineRestClient.getPipelineTimelineAsync(build.id);
34-
const changes = await this.pipelineRestClient.getPipelineChangesAsync(build.id);
35+
const timeline = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineTimelineAsync(build.id));
36+
const changes = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineChangesAsync(build.id));
3537
const phases = this.getPhases(timeline);
3638
const lastCompletedBuild = await this.pipelineRestClient.getLastPipelineAsync(build.definition.id, null, build.sourceBranch) as Build;
37-
const lastCompletedTimeline = lastCompletedBuild != null ? await this.pipelineRestClient.getPipelineTimelineAsync(lastCompletedBuild.id) : null;
39+
const lastCompletedTimeline = lastCompletedBuild != null ? await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineTimelineAsync(lastCompletedBuild.id)) : null;
3840

3941
console.log("Fetched release data");
4042
report.setBuildData(build, timeline, lastCompletedBuild, lastCompletedTimeline, phases, changes);
4143

4244
return report;
4345
}
4446

47+
private async getBuildAsync(pipelineConfig: PipelineConfiguration): Promise<Build> {
48+
var build = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineAsync());
49+
if(isNullOrUndefined(build)) {
50+
throw new DataProviderError(`Unable to find build with id: ${pipelineConfig.$pipelineId}`);
51+
}
52+
return build as Build;
53+
}
54+
4555
private getPhases(timeline: Timeline): PhaseModel[] {
4656
const records = timeline.records.sort( (a: TimelineRecord, b: TimelineRecord) => this.getOrder(a) - this.getOrder(b));
4757
const phases = records.filter(r => r.type == "Phase");

Tasks/emailReportTask/providers/pipeline/ReleaseDataProvider.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { ChangeModel } from "../../model/ChangeModel";
1414
import { ReleaseReport } from "../../model/ReleaseReport";
1515
import { ReportDataConfiguration } from "../../config/report/ReportDataConfiguration";
1616
import { ReportFactory } from "../../model/ReportFactory";
17+
import { RetryablePromise } from "../restclients/RetryablePromise";
18+
import { isNullOrUndefined } from "util";
1719

1820
export class ReleaseDataProvider implements IDataProvider {
1921

@@ -25,7 +27,7 @@ export class ReleaseDataProvider implements IDataProvider {
2527

2628
async getReportDataAsync(pipelineConfig: PipelineConfiguration, reportDataConfiguration: ReportDataConfiguration): Promise<Report> {
2729
const report = ReportFactory.createNewReport(pipelineConfig) as ReleaseReport;
28-
const release = await this.pipelineRestClient.getPipelineAsync() as Release;
30+
const release = await this.getReleaseAsync(pipelineConfig);
2931
if (release == null) {
3032
throw new PipelineNotFoundError(`ProjectId: ${pipelineConfig.$projectId}, ${pipelineConfig.$pipelineId}`);
3133
}
@@ -38,7 +40,7 @@ export class ReleaseDataProvider implements IDataProvider {
3840
// check if last completed one isn't latter one, then changes don't make sense
3941
if (lastCompletedRelease != null && lastCompletedRelease.id < release.id) {
4042
console.log(`Getting changes between releases ${release.id} & ${lastCompletedRelease.id}`);
41-
changes = await this.pipelineRestClient.getPipelineChangesAsync(lastCompletedRelease.id);
43+
changes = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineChangesAsync(lastCompletedRelease.id));
4244
}
4345
else {
4446
console.log("Unable to find any last completed release");
@@ -50,6 +52,14 @@ export class ReleaseDataProvider implements IDataProvider {
5052
return report;
5153
}
5254

55+
private async getReleaseAsync(pipelineConfig: PipelineConfiguration): Promise<Release> {
56+
var release = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineAsync());
57+
if(isNullOrUndefined(release)) {
58+
throw new DataProviderError(`Unable to find release with release id: ${pipelineConfig.$pipelineId}`);
59+
}
60+
return release as Release;
61+
}
62+
5363
private getEnvironment(release: Release, pipelineConfig: PipelineConfiguration): ReleaseEnvironment {
5464
let environment: ReleaseEnvironment;
5565
const environments = release.environments;
@@ -98,8 +108,9 @@ export class ReleaseDataProvider implements IDataProvider {
98108
}
99109

100110
console.log(`Fetching last release by completed environment id - ${pipelineConfig.$environmentId} and branch id ${branchId}`);
101-
const lastRelease = await this.pipelineRestClient.getLastPipelineAsync(release.releaseDefinition.id,
102-
environment.definitionEnvironmentId, branchId, null); //Bug in API - release.createdOn);
111+
const lastRelease = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getLastPipelineAsync(release.releaseDefinition.id,
112+
environment.definitionEnvironmentId, branchId, null)); //Bug in API - release.createdOn);
113+
103114
return lastRelease as Release;
104115
}
105116
}

Tasks/emailReportTask/providers/restclients/AbstractTestResultsClient.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ITestApi } from "azure-devops-node-api/TestApi";
44
import { TestResultsDetails, TestResultSummary, TestOutcome, TestResultsQuery, TestCaseResult, ResultsFilter, WorkItemReference } from "azure-devops-node-api/interfaces/TestInterfaces";
55
import { ITestResultsClient } from "./ITestResultsClient";
66
import { IdentityRef } from "azure-devops-node-api/interfaces/common/VSSInterfaces";
7+
import { RetryablePromise } from "./RetryablePromise";
78

89
export abstract class AbstractTestResultsClient extends AbstractClient implements ITestResultsClient {
910

@@ -16,26 +17,23 @@ export abstract class AbstractTestResultsClient extends AbstractClient implement
1617
}
1718

1819
public async queryTestResultBugs(automatedTestName: string, resultId: number): Promise<WorkItemReference[]> {
19-
return await (await this.testApiPromise).queryTestResultWorkItems(
20+
const testApi = await this.testApiPromise;
21+
return await RetryablePromise.RetryAsync(async () => testApi.queryTestResultWorkItems(
2022
this.pipelineConfig.$projectName,
2123
"Microsoft.BugCategory",
2224
automatedTestName,
2325
resultId
24-
);
26+
));
2527
}
2628

2729
public async getTestResultById(testRunId: number, resultId: number): Promise<TestCaseResult> {
28-
const result = await (await this.testApiPromise).getTestResultById(
29-
this.pipelineConfig.$projectName,
30-
testRunId,
31-
resultId
32-
);
33-
return result;
30+
const testApi = await this.testApiPromise;
31+
return await RetryablePromise.RetryAsync(async () => testApi.getTestResultById(this.pipelineConfig.$projectName, testRunId, resultId));
3432
}
3533

3634
public async queryTestResultsReportAsync(parameterConfig: PipelineConfiguration = null): Promise<TestResultSummary> {
3735
const config = parameterConfig != null ? parameterConfig : this.pipelineConfig;
38-
return await this.queryTestResultsReportForPipelineAsync(null, config);
36+
return await RetryablePromise.RetryAsync(async () => this.queryTestResultsReportForPipelineAsync(config));
3937
}
4038

4139
public async getTestResultOwnersAsync(resultsToFetch: TestCaseResult[]): Promise<IdentityRef[]> {
@@ -47,7 +45,7 @@ export abstract class AbstractTestResultsClient extends AbstractClient implement
4745
for (let i = 0, j = resultsToFetch.length; i < j; i += this.MaxItemsSupported) {
4846
const tempArray = resultsToFetch.slice(i, i + this.MaxItemsSupported);
4947
query.results = tempArray;
50-
tasks.push(testApi.getTestResultsByQuery(query, this.pipelineConfig.$projectName));
48+
tasks.push(RetryablePromise.RetryAsync(async () => testApi.getTestResultsByQuery(query, this.pipelineConfig.$projectName)));
5149
}
5250

5351
await Promise.all(tasks);
@@ -73,20 +71,20 @@ export abstract class AbstractTestResultsClient extends AbstractClient implement
7371
public async getTestResultsDetailsAsync(groupBy: string, outcomeFilters?: TestOutcome[], parameterConfig: PipelineConfiguration = null): Promise<TestResultsDetails> {
7472
const filter = this.getOutcomeFilter(outcomeFilters);
7573
const config = parameterConfig != null ? parameterConfig : this.pipelineConfig;
76-
return await this.getTestResultsDetailsForPipelineAsync(groupBy, filter, config);
74+
return await RetryablePromise.RetryAsync(async () => this.getTestResultsDetailsForPipelineAsync(config, groupBy, filter));
7775
}
7876

7977
public async getTestResultSummaryAsync(includeFailures: boolean, parameterConfig: PipelineConfiguration = null): Promise<TestResultSummary> {
8078
const config = parameterConfig != null ? parameterConfig : this.pipelineConfig;
81-
return await this.queryTestResultsReportForPipelineAsync(includeFailures, config);
79+
return await RetryablePromise.RetryAsync(async () => this.queryTestResultsReportForPipelineAsync(config, includeFailures));
8280
}
8381

8482
public async getTestResultsByQueryAsync(query: TestResultsQuery): Promise<TestResultsQuery> {
8583
return await (await this.testApiPromise).getTestResultsByQuery(query, this.pipelineConfig.$projectId);
8684
}
8785

88-
protected abstract getTestResultsDetailsForPipelineAsync(groupBy: string, filter: string, config: PipelineConfiguration): Promise<TestResultsDetails>;
89-
protected abstract queryTestResultsReportForPipelineAsync(includeFailures: boolean, config: PipelineConfiguration): Promise<TestResultSummary>;
86+
protected abstract getTestResultsDetailsForPipelineAsync(config: PipelineConfiguration, groupBy?: string, filter?: string): Promise<TestResultsDetails>;
87+
protected abstract queryTestResultsReportForPipelineAsync(config: PipelineConfiguration, includeFailures?: boolean): Promise<TestResultSummary>;
9088

9189
protected getOutcomeFilter(outcomes: TestOutcome[]): string {
9290
let filter: string = null;

Tasks/emailReportTask/providers/restclients/BuildTestResultsClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ export class BuildTestResultsClient extends AbstractTestResultsClient implements
99
super(pipelineConfig);
1010
}
1111

12-
public async queryTestResultsReportForPipelineAsync(includeFailures: boolean, config: PipelineConfiguration): Promise<TestResultSummary> {
12+
public async queryTestResultsReportForPipelineAsync(config: PipelineConfiguration, includeFailures?: boolean): Promise<TestResultSummary> {
1313
return await (await this.testApiPromise).queryTestResultsReportForBuild(
1414
config.$projectName,
1515
config.$pipelineId,
1616
null,
1717
includeFailures);
1818
}
1919

20-
public async getTestResultsDetailsForPipelineAsync(groupBy: string, filter: string, config: PipelineConfiguration): Promise<TestResultsDetails> {
20+
public async getTestResultsDetailsForPipelineAsync(config: PipelineConfiguration, groupBy?: string, filter?: string): Promise<TestResultsDetails> {
2121
return await (await this.testApiPromise).getTestResultDetailsForBuild(
2222
config.$projectName,
2323
config.$pipelineId,
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { Release } from "azure-devops-node-api/interfaces/ReleaseInterfaces";
2-
import { Build, Timeline } from "azure-devops-node-api/interfaces/BuildInterfaces";
1+
import { Timeline } from "azure-devops-node-api/interfaces/BuildInterfaces";
32
import { ChangeModel } from "../../model/ChangeModel";
43

54
export interface IPipelineRestClient {
6-
getPipelineAsync(): Promise<Release> | Promise<Build>;
7-
getLastPipelineAsync(pipelineDefId: number, envDefId: number, sourceBranchFilter: string, maxCreatedDate?: Date): Promise<Release> | Promise<Build>;
5+
getPipelineAsync(): Promise<any>;
6+
getLastPipelineAsync(pipelineDefId: number, envDefId: number, sourceBranchFilter: string, maxCreatedDate?: Date): Promise<any>;
87
getPipelineChangesAsync(prevPipelineId: number): Promise<ChangeModel[]>;
98
getPipelineTimelineAsync(pipelineId: number): Promise<Timeline>;
109
}

Tasks/emailReportTask/providers/restclients/ReleaseClient.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ export class ReleaseRestClient extends AbstractClient implements IPipelineRestCl
2424
sourceBranchFilter: string,
2525
maxCreatedDate?: Date
2626
): Promise<Release> {
27+
const releaseApi = await this.connection.getReleaseApi();
2728
let lastRelease: Release = null;
2829
const releaseStatusFilter = ReleaseStatus.Active;
2930
const envStatusFilter = EnvironmentStatus.Succeeded | EnvironmentStatus.PartiallySucceeded | EnvironmentStatus.Rejected | EnvironmentStatus.Canceled;
30-
const releases = await (await this.connection.getReleaseApi()).getReleases(
31+
const releases = await releaseApi.getReleases(
3132
this.pipelineConfig.$projectId,
3233
pipelineDefId,
3334
envDefId,
@@ -60,6 +61,11 @@ export class ReleaseRestClient extends AbstractClient implements IPipelineRestCl
6061

6162
if (isNullOrUndefined(lastRelease)) {
6263
console.log(`Unable to fetch last completed release for release definition:${pipelineDefId} and environmentid: ${envDefId}`);
64+
} else if(lastRelease.id < this.pipelineConfig.$pipelineId) {
65+
return await releaseApi.getRelease(
66+
this.pipelineConfig.$projectId,
67+
lastRelease.id
68+
);
6369
}
6470

6571
return lastRelease;

0 commit comments

Comments
 (0)