Skip to content

Commit 96b7722

Browse files
authored
Add logic to stop an evaluation run (#3421)
1 parent df78259 commit 96b7722

4 files changed

Lines changed: 207 additions & 32 deletions

File tree

extensions/ql-vscode/src/model-editor/model-evaluator.ts

Lines changed: 98 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,22 @@ import type { CodeQLCliServer } from "../codeql-cli/cli";
99
import type { VariantAnalysisManager } from "../variant-analysis/variant-analysis-manager";
1010
import type { QueryLanguage } from "../common/query-language";
1111
import { resolveCodeScanningQueryPack } from "../variant-analysis/code-scanning-pack";
12-
import { withProgress } from "../common/vscode/progress";
12+
import type { ProgressCallback } from "../common/vscode/progress";
13+
import {
14+
UserCancellationException,
15+
withProgress,
16+
} from "../common/vscode/progress";
1317
import type { VariantAnalysis } from "../variant-analysis/shared/variant-analysis";
18+
import type { CancellationToken } from "vscode";
19+
import { CancellationTokenSource } from "vscode";
20+
import type { QlPackDetails } from "../variant-analysis/ql-pack-details";
1421

1522
export class ModelEvaluator extends DisposableObject {
23+
// Cancellation token source to allow cancelling of the current run
24+
// before a variant analysis has been submitted. Once it has been
25+
// submitted, we use the variant analysis manager's cancellation support.
26+
private cancellationSource: CancellationTokenSource;
27+
1628
public constructor(
1729
private readonly logger: BaseLogger,
1830
private readonly cliServer: CodeQLCliServer,
@@ -28,6 +40,8 @@ export class ModelEvaluator extends DisposableObject {
2840
super();
2941

3042
this.registerToModelingEvents();
43+
44+
this.cancellationSource = new CancellationTokenSource();
3145
}
3246

3347
public async startEvaluation() {
@@ -52,30 +66,12 @@ export class ModelEvaluator extends DisposableObject {
5266

5367
// Submit variant analysis and monitor progress
5468
return withProgress(
55-
async (progress, token) => {
56-
let variantAnalysisId: number | undefined = undefined;
57-
try {
58-
variantAnalysisId =
59-
await this.variantAnalysisManager.runVariantAnalysis(
60-
qlPack,
61-
progress,
62-
token,
63-
false,
64-
);
65-
} catch (e) {
66-
this.modelingStore.updateModelEvaluationRun(this.dbItem, undefined);
67-
throw e;
68-
}
69-
70-
if (variantAnalysisId) {
71-
this.monitorVariantAnalysis(variantAnalysisId);
72-
} else {
73-
this.modelingStore.updateModelEvaluationRun(this.dbItem, undefined);
74-
throw new Error(
75-
"Unable to trigger variant analysis for evaluation run",
76-
);
77-
}
78-
},
69+
(progress) =>
70+
this.runVariantAnalysis(
71+
qlPack,
72+
progress,
73+
this.cancellationSource.token,
74+
),
7975
{
8076
title: "Run model evaluation",
8177
cancellable: false,
@@ -84,13 +80,29 @@ export class ModelEvaluator extends DisposableObject {
8480
}
8581

8682
public async stopEvaluation() {
87-
// For now just update the store.
88-
// This will be fleshed out in the near future.
89-
const evaluationRun: ModelEvaluationRun = {
90-
isPreparing: false,
91-
variantAnalysisId: undefined,
92-
};
93-
this.modelingStore.updateModelEvaluationRun(this.dbItem, evaluationRun);
83+
const evaluationRun = this.modelingStore.getModelEvaluationRun(this.dbItem);
84+
if (!evaluationRun) {
85+
void this.logger.log("No active evaluation run to stop");
86+
return;
87+
}
88+
89+
this.cancellationSource.cancel();
90+
91+
if (evaluationRun.variantAnalysisId === undefined) {
92+
// If the variant analysis has not been submitted yet, we can just
93+
// update the store.
94+
this.modelingStore.updateModelEvaluationRun(this.dbItem, {
95+
...evaluationRun,
96+
isPreparing: false,
97+
});
98+
} else {
99+
// If the variant analysis has been submitted, we need to cancel it. We
100+
// don't need to update the store here, as the event handler for
101+
// onVariantAnalysisStatusUpdated will do that for us.
102+
await this.variantAnalysisManager.cancelVariantAnalysis(
103+
evaluationRun.variantAnalysisId,
104+
);
105+
}
94106
}
95107

96108
private registerToModelingEvents() {
@@ -128,6 +140,60 @@ export class ModelEvaluator extends DisposableObject {
128140
return undefined;
129141
}
130142

143+
private async runVariantAnalysis(
144+
qlPack: QlPackDetails,
145+
progress: ProgressCallback,
146+
token: CancellationToken,
147+
): Promise<number | void> {
148+
let result: number | void = undefined;
149+
try {
150+
// Use Promise.race to make sure to stop the variant analysis processing when the
151+
// user has stopped the evaluation run. We can't simply rely on the cancellation token
152+
// because we haven't fully implemented cancellation support for variant analysis.
153+
// Using this approach we make sure that the process is stopped from a user's point
154+
// of view (the notification goes away too). It won't necessarily stop any tasks
155+
// that are not aware of the cancellation token.
156+
result = await Promise.race([
157+
this.variantAnalysisManager.runVariantAnalysis(
158+
qlPack,
159+
progress,
160+
token,
161+
false,
162+
),
163+
new Promise<void>((_, reject) => {
164+
token.onCancellationRequested(() =>
165+
reject(new UserCancellationException(undefined, true)),
166+
);
167+
}),
168+
]);
169+
} catch (e) {
170+
this.modelingStore.updateModelEvaluationRun(this.dbItem, undefined);
171+
if (!(e instanceof UserCancellationException)) {
172+
throw e;
173+
} else {
174+
return;
175+
}
176+
} finally {
177+
// Renew the cancellation token source for the new evaluation run.
178+
// This is necessary because we don't want the next evaluation run
179+
// to start as cancelled.
180+
this.cancellationSource = new CancellationTokenSource();
181+
}
182+
183+
// If the result is a number, it means the variant analysis was successfully submitted,
184+
// so we need to update the store and start up the monitor.
185+
if (typeof result === "number") {
186+
this.modelingStore.updateModelEvaluationRun(this.dbItem, {
187+
isPreparing: true,
188+
variantAnalysisId: result,
189+
});
190+
this.monitorVariantAnalysis(result);
191+
} else {
192+
this.modelingStore.updateModelEvaluationRun(this.dbItem, undefined);
193+
throw new Error("Unable to trigger variant analysis for evaluation run");
194+
}
195+
}
196+
131197
private monitorVariantAnalysis(variantAnalysisId: number) {
132198
this.push(
133199
this.variantAnalysisManager.onVariantAnalysisStatusUpdated(

extensions/ql-vscode/src/model-editor/modeling-store.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,12 @@ export class ModelingStore extends DisposableObject {
423423
return this.state.get(databaseItem.databaseUri.toString())!;
424424
}
425425

426+
public getModelEvaluationRun(
427+
dbItem: DatabaseItem,
428+
): ModelEvaluationRun | undefined {
429+
return this.getState(dbItem).modelEvaluationRun;
430+
}
431+
426432
private changeMethods(
427433
dbItem: DatabaseItem,
428434
updateState: (state: InternalDbModelingState) => void,

extensions/ql-vscode/test/__mocks__/model-editor/modelingStoreMock.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ import type { ModelingStore } from "../../../src/model-editor/modeling-store";
44
export function createMockModelingStore({
55
initializeStateForDb = jest.fn(),
66
getStateForActiveDb = jest.fn(),
7+
getModelEvaluationRun = jest.fn(),
8+
updateModelEvaluationRun = jest.fn(),
79
}: {
810
initializeStateForDb?: ModelingStore["initializeStateForDb"];
911
getStateForActiveDb?: ModelingStore["getStateForActiveDb"];
12+
getModelEvaluationRun?: ModelingStore["getModelEvaluationRun"];
13+
updateModelEvaluationRun?: ModelingStore["updateModelEvaluationRun"];
1014
} = {}): ModelingStore {
1115
return mockedObject<ModelingStore>({
1216
initializeStateForDb,
1317
getStateForActiveDb,
18+
getModelEvaluationRun,
19+
updateModelEvaluationRun,
1420
});
1521
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
2+
import type { BaseLogger } from "../../../../src/common/logging";
3+
import { QueryLanguage } from "../../../../src/common/query-language";
4+
import type { DatabaseItem } from "../../../../src/databases/local-databases";
5+
import type { ModelEvaluationRun } from "../../../../src/model-editor/model-evaluation-run";
6+
import { ModelEvaluator } from "../../../../src/model-editor/model-evaluator";
7+
import type { ModelingEvents } from "../../../../src/model-editor/modeling-events";
8+
import type { ModelingStore } from "../../../../src/model-editor/modeling-store";
9+
import type { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager";
10+
import { createMockLogger } from "../../../__mocks__/loggerMock";
11+
import { createMockModelingEvents } from "../../../__mocks__/model-editor/modelingEventsMock";
12+
import { createMockModelingStore } from "../../../__mocks__/model-editor/modelingStoreMock";
13+
import { mockedObject } from "../../../mocked-object";
14+
15+
describe("Model Evaluator", () => {
16+
let modelEvaluator: ModelEvaluator;
17+
let logger: BaseLogger;
18+
let cliServer: CodeQLCliServer;
19+
let modelingStore: ModelingStore;
20+
let modelingEvents: ModelingEvents;
21+
let variantAnalysisManager: VariantAnalysisManager;
22+
let dbItem: DatabaseItem;
23+
let language: QueryLanguage;
24+
let updateView: jest.Mock;
25+
let getModelEvaluationRunMock = jest.fn();
26+
27+
beforeEach(() => {
28+
logger = createMockLogger();
29+
cliServer = mockedObject<CodeQLCliServer>({});
30+
getModelEvaluationRunMock = jest.fn();
31+
modelingStore = createMockModelingStore({
32+
getModelEvaluationRun: getModelEvaluationRunMock,
33+
});
34+
modelingEvents = createMockModelingEvents();
35+
variantAnalysisManager = mockedObject<VariantAnalysisManager>({
36+
cancelVariantAnalysis: jest.fn(),
37+
});
38+
dbItem = mockedObject<DatabaseItem>({});
39+
language = QueryLanguage.Java;
40+
updateView = jest.fn();
41+
42+
modelEvaluator = new ModelEvaluator(
43+
logger,
44+
cliServer,
45+
modelingStore,
46+
modelingEvents,
47+
variantAnalysisManager,
48+
dbItem,
49+
language,
50+
updateView,
51+
);
52+
});
53+
54+
describe("stopping evaluation", () => {
55+
it("should just log a message if it never started", async () => {
56+
getModelEvaluationRunMock.mockReturnValue(undefined);
57+
58+
await modelEvaluator.stopEvaluation();
59+
60+
expect(logger.log).toHaveBeenCalledWith(
61+
"No active evaluation run to stop",
62+
);
63+
});
64+
65+
it("should update the store if evaluation run exists", async () => {
66+
getModelEvaluationRunMock.mockReturnValue({
67+
isPreparing: true,
68+
variantAnalysisId: undefined,
69+
});
70+
71+
await modelEvaluator.stopEvaluation();
72+
73+
expect(modelingStore.updateModelEvaluationRun).toHaveBeenCalledWith(
74+
dbItem,
75+
{
76+
isPreparing: false,
77+
varianAnalysis: undefined,
78+
},
79+
);
80+
});
81+
82+
it("should cancel the variant analysis if one has been started", async () => {
83+
const evaluationRun: ModelEvaluationRun = {
84+
isPreparing: false,
85+
variantAnalysisId: 123,
86+
};
87+
getModelEvaluationRunMock.mockReturnValue(evaluationRun);
88+
89+
await modelEvaluator.stopEvaluation();
90+
91+
expect(modelingStore.updateModelEvaluationRun).not.toHaveBeenCalled();
92+
expect(variantAnalysisManager.cancelVariantAnalysis).toHaveBeenCalledWith(
93+
evaluationRun.variantAnalysisId,
94+
);
95+
});
96+
});
97+
});

0 commit comments

Comments
 (0)