Make sure the syntax and semantic servers don't get out of sync

If one server fails for a command but the other does not, we are in an inconsistent state. Treat this as a fatal error
This commit is contained in:
Matt Bierner 2019-10-21 17:38:31 -07:00
parent ab4e86df8d
commit ed53e86205
3 changed files with 56 additions and 11 deletions

View file

@ -59,6 +59,10 @@ export interface ITypeScriptServer {
dispose(): void;
}
export interface TsServerDelegate {
onFatalError(command: string): void;
}
export interface TsServerProcess {
readonly stdout: stream.Readable;
write(serverRequest: Proto.Request): void;
@ -305,6 +309,7 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
public constructor(
private readonly syntaxServer: ITypeScriptServer,
private readonly semanticServer: ITypeScriptServer,
private readonly _delegate: TsServerDelegate,
) {
super();
@ -362,14 +367,18 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
} else if (SyntaxRoutingTsServer.sharedCommands.has(command)) {
// Dispatch to both server but only return from syntax one
const enum RequestState { Unresolved, Resolved, Errored }
let syntaxRequestState = RequestState.Unresolved;
let semanticRequestState = RequestState.Unresolved;
// Also make sure we never cancel requests to just one server
let hasCompletedSyntax = false;
let hasCompletedSemantic = false;
let token: vscode.CancellationToken | undefined = undefined;
if (executeInfo.token) {
const source = new vscode.CancellationTokenSource();
executeInfo.token.onCancellationRequested(() => {
if (hasCompletedSyntax && !hasCompletedSemantic || hasCompletedSemantic && !hasCompletedSyntax) {
if (syntaxRequestState !== RequestState.Unresolved && semanticRequestState === RequestState.Unresolved
|| syntaxRequestState === RequestState.Unresolved && semanticRequestState !== RequestState.Unresolved
) {
// Don't cancel.
// One of the servers completed this request so we don't want to leave the other
// in a different state
@ -382,11 +391,41 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
const semanticRequest = this.semanticServer.executeImpl(command, args, { ...executeInfo, token });
if (semanticRequest) {
semanticRequest.finally(() => { hasCompletedSemantic = true; });
semanticRequest
.then(result => {
semanticRequestState = RequestState.Resolved;
if (syntaxRequestState === RequestState.Errored) {
// We've gone out of sync
this._delegate.onFatalError(command);
}
return result;
}, err => {
semanticRequestState = RequestState.Errored;
if (syntaxRequestState === RequestState.Resolved) {
// We've gone out of sync
this._delegate.onFatalError(command);
}
throw err;
});
}
const syntaxRequest = this.syntaxServer.executeImpl(command, args, { ...executeInfo, token });
if (syntaxRequest) {
syntaxRequest.finally(() => { hasCompletedSyntax = true; });
syntaxRequest
.then(result => {
syntaxRequestState = RequestState.Resolved;
if (semanticRequestState === RequestState.Errored) {
// We've gone out of sync
this._delegate.onFatalError(command);
}
return result;
}, err => {
syntaxRequestState = RequestState.Errored;
if (semanticRequestState === RequestState.Resolved) {
// We've gone out of sync
this._delegate.onFatalError(command);
}
throw err;
});
}
return syntaxRequest;
} else {

View file

@ -18,7 +18,7 @@ import { PluginManager } from '../utils/plugins';
import TelemetryReporter from '../utils/telemetry';
import Tracer from '../utils/tracer';
import { TypeScriptVersion, TypeScriptVersionProvider } from '../utils/versionProvider';
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess } from './server';
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess, TsServerDelegate } from './server';
type ServerKind = 'main' | 'syntax' | 'semantic';
@ -35,12 +35,13 @@ export class TypeScriptServerSpawner {
public spawn(
version: TypeScriptVersion,
configuration: TypeScriptServiceConfiguration,
pluginManager: PluginManager
pluginManager: PluginManager,
delegate: TsServerDelegate,
): ITypeScriptServer {
if (this.shouldUseSeparateSyntaxServer(version, configuration)) {
const syntaxServer = this.spawnTsServer('syntax', version, configuration, pluginManager);
const semanticServer = this.spawnTsServer('semantic', version, configuration, pluginManager);
return new SyntaxRoutingTsServer(syntaxServer, semanticServer);
return new SyntaxRoutingTsServer(syntaxServer, semanticServer, delegate);
}
return this.spawnTsServer('main', version, configuration, pluginManager);
@ -65,7 +66,7 @@ export class TypeScriptServerSpawner {
if (TypeScriptServerSpawner.isLoggingEnabled(apiVersion, configuration)) {
if (tsServerLogFile) {
this._logger.info(`<${kind}> Log file: ${tsServerLogFile}`);
this._logger.info(`<${kind}> Log file: ${tsServerLogFile}`);
} else {
this._logger.error(`<${kind}> Could not create log directory`);
}

View file

@ -288,7 +288,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType
const apiVersion = this.versionPicker.currentVersion.apiVersion || API.defaultVersion;
this.onDidChangeTypeScriptVersion(currentVersion);
let mytoken = ++this.token;
const handle = this.typescriptServerSpawner.spawn(currentVersion, this.configuration, this.pluginManager);
const handle = this.typescriptServerSpawner.spawn(currentVersion, this.configuration, this.pluginManager, {
onFatalError: (command) => this.fatalError(command),
});
this.serverState = new ServerState.Running(handle, apiVersion, undefined, true);
this.lastStart = Date.now();
@ -665,7 +667,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this.logTelemetry('fatalError', { command });
console.error(`A non-recoverable error occured while executing tsserver command: ${command}`);
this.restartTsServer();
if (this.serverState.type === ServerState.Type.Running) {
this.info('Killing TS Server');
this.serverState.server.kill();
}
}
private dispatchEvent(event: Proto.Event) {