Skip to content

Commit 81f4e38

Browse files
authored
Enable per-request cancellation (#12371)
enable -per-request cancellation * restore request for deferred calls * add tests * introduce MultistepOperation * (test) subsequent request cancels the preceding one
1 parent 1f484a9 commit 81f4e38

File tree

9 files changed

+513
-85
lines changed

9 files changed

+513
-85
lines changed

src/harness/harnessLanguageService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ namespace Harness.LanguageService {
738738
// host to answer server queries about files on disk
739739
const serverHost = new SessionServerHost(clientHost);
740740
const server = new ts.server.Session(serverHost,
741-
{ isCancellationRequested: () => false },
741+
ts.server.nullCancellationToken,
742742
/*useOneInferredProject*/ false,
743743
/*typingsInstaller*/ undefined,
744744
Utils.byteLength,

src/harness/unittests/compileOnSave.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace ts.projectSystem {
66
import CommandNames = server.CommandNames;
7+
const nullCancellationToken = server.nullCancellationToken;
78

89
function createTestTypingsInstaller(host: server.ServerHost) {
910
return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host);

src/harness/unittests/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ namespace ts.server {
2727
clearImmediate: noop,
2828
createHash: s => s
2929
};
30-
const nullCancellationToken: HostCancellationToken = { isCancellationRequested: () => false };
30+
3131
const mockLogger: Logger = {
3232
close: noop,
3333
hasLevel(): boolean { return false; },

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 231 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ namespace ts.projectSystem {
3434
getLogFileName: (): string => undefined
3535
};
3636

37-
export const nullCancellationToken: HostCancellationToken = {
38-
isCancellationRequested: () => false
39-
};
40-
4137
export const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
4238
export const libFile: FileOrFolder = {
4339
path: "/a/lib/lib.d.ts",
@@ -158,17 +154,33 @@ namespace ts.projectSystem {
158154
}
159155

160156
class TestSession extends server.Session {
157+
private seq = 0;
158+
161159
getProjectService() {
162160
return this.projectService;
163161
}
162+
163+
public getSeq() {
164+
return this.seq;
165+
}
166+
167+
public getNextSeq() {
168+
return this.seq + 1;
169+
}
170+
171+
public executeCommandSeq<T extends server.protocol.Request>(request: Partial<T>) {
172+
this.seq++;
173+
request.seq = this.seq;
174+
request.type = "request";
175+
return this.executeCommand(<T>request);
176+
}
164177
};
165178

166-
export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller, projectServiceEventHandler?: server.ProjectServiceEventHandler) {
179+
export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller, projectServiceEventHandler?: server.ProjectServiceEventHandler, cancellationToken?: server.ServerCancellationToken) {
167180
if (typingsInstaller === undefined) {
168181
typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host);
169182
}
170-
171-
return new TestSession(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ projectServiceEventHandler !== undefined, projectServiceEventHandler);
183+
return new TestSession(host, cancellationToken || server.nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ projectServiceEventHandler !== undefined, projectServiceEventHandler);
172184
}
173185

174186
export interface CreateProjectServiceParameters {
@@ -191,7 +203,7 @@ namespace ts.projectSystem {
191203
}
192204
}
193205
export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}) {
194-
const cancellationToken = parameters.cancellationToken || nullCancellationToken;
206+
const cancellationToken = parameters.cancellationToken || server.nullCancellationToken;
195207
const logger = parameters.logger || nullLogger;
196208
const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false;
197209
return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller, parameters.eventHandler);
@@ -328,6 +340,8 @@ namespace ts.projectSystem {
328340
export class TestServerHost implements server.ServerHost {
329341
args: string[] = [];
330342

343+
private readonly output: string[] = [];
344+
331345
private fs: ts.FileMap<FSEntry>;
332346
private getCanonicalFileName: (s: string) => string;
333347
private toPath: (f: string) => Path;
@@ -477,6 +491,10 @@ namespace ts.projectSystem {
477491
this.timeoutCallbacks.invoke();
478492
}
479493

494+
runQueuedImmediateCallbacks() {
495+
this.immediateCallbacks.invoke();
496+
}
497+
480498
setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) {
481499
return this.immediateCallbacks.register(callback, args);
482500
}
@@ -509,7 +527,17 @@ namespace ts.projectSystem {
509527
this.reloadFS(filesOrFolders);
510528
}
511529

512-
write() { }
530+
write(message: string) {
531+
this.output.push(message);
532+
}
533+
534+
getOutput(): ReadonlyArray<string> {
535+
return this.output;
536+
}
537+
538+
clearOutput() {
539+
this.output.length = 0;
540+
}
513541

514542
readonly readFile = (s: string) => (<File>this.fs.get(this.toPath(s))).content;
515543
readonly resolvePath = (s: string) => s;
@@ -3131,6 +3159,200 @@ namespace ts.projectSystem {
31313159
});
31323160
});
31333161

3162+
describe("cancellationToken", () => {
3163+
it("is attached to request", () => {
3164+
const f1 = {
3165+
path: "/a/b/app.ts",
3166+
content: "let xyz = 1;"
3167+
};
3168+
const host = createServerHost([f1]);
3169+
let expectedRequestId: number;
3170+
const cancellationToken: server.ServerCancellationToken = {
3171+
isCancellationRequested: () => false,
3172+
setRequest: requestId => {
3173+
if (expectedRequestId === undefined) {
3174+
assert.isTrue(false, "unexpected call")
3175+
}
3176+
assert.equal(requestId, expectedRequestId);
3177+
},
3178+
resetRequest: noop
3179+
}
3180+
const session = createSession(host, /*typingsInstaller*/ undefined, /*projectServiceEventHandler*/ undefined, cancellationToken);
3181+
3182+
expectedRequestId = session.getNextSeq();
3183+
session.executeCommandSeq(<server.protocol.OpenRequest>{
3184+
command: "open",
3185+
arguments: { file: f1.path }
3186+
});
3187+
3188+
expectedRequestId = session.getNextSeq();
3189+
session.executeCommandSeq(<server.protocol.GeterrRequest>{
3190+
command: "geterr",
3191+
arguments: { files: [f1.path] }
3192+
});
3193+
3194+
expectedRequestId = session.getNextSeq();
3195+
session.executeCommandSeq(<server.protocol.OccurrencesRequest>{
3196+
command: "occurrences",
3197+
arguments: { file: f1.path, line: 1, offset: 6 }
3198+
});
3199+
3200+
expectedRequestId = 2;
3201+
host.runQueuedImmediateCallbacks();
3202+
expectedRequestId = 2;
3203+
host.runQueuedImmediateCallbacks();
3204+
});
3205+
3206+
it("Geterr is cancellable", () => {
3207+
const f1 = {
3208+
path: "/a/app.ts",
3209+
content: "let x = 1"
3210+
};
3211+
const config = {
3212+
path: "/a/tsconfig.json",
3213+
content: JSON.stringify({
3214+
compilerOptions: {}
3215+
})
3216+
};
3217+
3218+
let requestToCancel = -1;
3219+
const cancellationToken: server.ServerCancellationToken = (function(){
3220+
let currentId: number;
3221+
return <server.ServerCancellationToken>{
3222+
setRequest(requestId) {
3223+
currentId = requestId;
3224+
},
3225+
resetRequest(requestId) {
3226+
assert.equal(requestId, currentId, "unexpected request id in cancellation")
3227+
currentId = undefined;
3228+
},
3229+
isCancellationRequested() {
3230+
return requestToCancel === currentId;
3231+
}
3232+
}
3233+
})();
3234+
const host = createServerHost([f1, config]);
3235+
const session = createSession(host, /*typingsInstaller*/ undefined, () => {}, cancellationToken);
3236+
{
3237+
session.executeCommandSeq(<protocol.OpenRequest>{
3238+
command: "open",
3239+
arguments: { file: f1.path }
3240+
});
3241+
// send geterr for missing file
3242+
session.executeCommandSeq(<protocol.GeterrRequest>{
3243+
command: "geterr",
3244+
arguments: { files: ["/a/missing"] }
3245+
});
3246+
// no files - expect 'completed' event
3247+
assert.equal(host.getOutput().length, 1, "expect 1 message");
3248+
verifyRequestCompleted(session.getSeq(), 0);
3249+
}
3250+
{
3251+
const getErrId = session.getNextSeq();
3252+
// send geterr for a valid file
3253+
session.executeCommandSeq(<protocol.GeterrRequest>{
3254+
command: "geterr",
3255+
arguments: { files: [f1.path] }
3256+
});
3257+
3258+
assert.equal(host.getOutput().length, 0, "expect 0 messages");
3259+
3260+
// run new request
3261+
session.executeCommandSeq(<protocol.ProjectInfoRequest>{
3262+
command: "projectInfo",
3263+
arguments: { file: f1.path }
3264+
});
3265+
host.clearOutput();
3266+
3267+
// cancel previously issued Geterr
3268+
requestToCancel = getErrId;
3269+
host.runQueuedTimeoutCallbacks();
3270+
3271+
assert.equal(host.getOutput().length, 1, "expect 1 message");
3272+
verifyRequestCompleted(getErrId, 0);
3273+
3274+
requestToCancel = -1;
3275+
}
3276+
{
3277+
const getErrId = session.getNextSeq();
3278+
session.executeCommandSeq(<protocol.GeterrRequest>{
3279+
command: "geterr",
3280+
arguments: { files: [f1.path] }
3281+
});
3282+
assert.equal(host.getOutput().length, 0, "expect 0 messages");
3283+
3284+
// run first step
3285+
host.runQueuedTimeoutCallbacks();
3286+
assert.equal(host.getOutput().length, 1, "expect 1 messages");
3287+
const e1 = <protocol.Event>getMessage(0);
3288+
assert.equal(e1.event, "syntaxDiag");
3289+
host.clearOutput();
3290+
3291+
requestToCancel = getErrId;
3292+
host.runQueuedImmediateCallbacks();
3293+
assert.equal(host.getOutput().length, 1, "expect 1 message");
3294+
verifyRequestCompleted(getErrId, 0);
3295+
3296+
requestToCancel = -1;
3297+
}
3298+
{
3299+
const getErrId = session.getNextSeq();
3300+
session.executeCommandSeq(<protocol.GeterrRequest>{
3301+
command: "geterr",
3302+
arguments: { files: [f1.path] }
3303+
});
3304+
assert.equal(host.getOutput().length, 0, "expect 0 messages");
3305+
3306+
// run first step
3307+
host.runQueuedTimeoutCallbacks();
3308+
assert.equal(host.getOutput().length, 1, "expect 1 messages");
3309+
const e1 = <protocol.Event>getMessage(0);
3310+
assert.equal(e1.event, "syntaxDiag");
3311+
host.clearOutput();
3312+
3313+
host.runQueuedImmediateCallbacks();
3314+
assert.equal(host.getOutput().length, 2, "expect 2 messages");
3315+
const e2 = <protocol.Event>getMessage(0);
3316+
assert.equal(e2.event, "semanticDiag");
3317+
verifyRequestCompleted(getErrId, 1);
3318+
3319+
requestToCancel = -1;
3320+
}
3321+
{
3322+
const getErr1 = session.getNextSeq();
3323+
session.executeCommandSeq(<protocol.GeterrRequest>{
3324+
command: "geterr",
3325+
arguments: { files: [f1.path] }
3326+
});
3327+
assert.equal(host.getOutput().length, 0, "expect 0 messages");
3328+
// run first step
3329+
host.runQueuedTimeoutCallbacks();
3330+
assert.equal(host.getOutput().length, 1, "expect 1 messages");
3331+
const e1 = <protocol.Event>getMessage(0);
3332+
assert.equal(e1.event, "syntaxDiag");
3333+
host.clearOutput();
3334+
3335+
session.executeCommandSeq(<protocol.GeterrRequest>{
3336+
command: "geterr",
3337+
arguments: { files: [f1.path] }
3338+
});
3339+
// make sure that getErr1 is completed
3340+
verifyRequestCompleted(getErr1, 0);
3341+
}
3342+
3343+
function verifyRequestCompleted(expectedSeq: number, n: number) {
3344+
const event = <protocol.RequestCompletedEvent>getMessage(n);
3345+
assert.equal(event.event, "requestCompleted");
3346+
assert.equal(event.body.request_seq, expectedSeq, "expectedSeq");
3347+
host.clearOutput();
3348+
}
3349+
3350+
function getMessage(n: number) {
3351+
return JSON.parse(server.extractMessage(host.getOutput()[n]));
3352+
}
3353+
});
3354+
});
3355+
31343356
describe("maxNodeModuleJsDepth for inferred projects", () => {
31353357
it("should be set to 2 if the project has js root files", () => {
31363358
const file1: FileOrFolder = {
@@ -3184,5 +3406,4 @@ namespace ts.projectSystem {
31843406
assert.isUndefined(project.getCompilerOptions().maxNodeModuleJsDepth);
31853407
});
31863408
});
3187-
31883409
}

0 commit comments

Comments
 (0)