Wrap shared process in retry
This commit is contained in:
parent
5d02194048
commit
499798fc17
@ -41,20 +41,30 @@ export interface IProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary notification service.
|
* Console-based notification service.
|
||||||
*/
|
*/
|
||||||
export class NotificationService implements INotificationService {
|
export class NotificationService implements INotificationService {
|
||||||
public error(error: Error): void {
|
public error(error: Error): void {
|
||||||
logger.error(error.message, field("error", error));
|
logger.error(error.message, field("error", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
public prompt(_severity: Severity, message: string, _buttons: INotificationButton[], _onCancel: () => void): INotificationHandle {
|
public prompt(severity: Severity, message: string, _buttons: INotificationButton[], _onCancel: () => void): INotificationHandle {
|
||||||
throw new Error(`cannot prompt using the console: ${message}`);
|
switch (severity) {
|
||||||
|
case Severity.Info: logger.info(message); break;
|
||||||
|
case Severity.Warning: logger.warn(message); break;
|
||||||
|
case Severity.Error: logger.error(message); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: (): void => undefined,
|
||||||
|
updateMessage: (): void => undefined,
|
||||||
|
updateButtons: (): void => undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary progress service.
|
* Console-based progress service.
|
||||||
*/
|
*/
|
||||||
export class ProgressService implements IProgressService {
|
export class ProgressService implements IProgressService {
|
||||||
public start<T>(title: string, task: (progress: IProgress) => Promise<T>): Promise<T> {
|
public start<T>(title: string, task: (progress: IProgress) => Promise<T>): Promise<T> {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { logger } from "@coder/logger";
|
import { logger, field } from "@coder/logger";
|
||||||
import { NotificationService, INotificationHandle, INotificationService, Severity } from "./fill/notification";
|
import { NotificationService, INotificationHandle, INotificationService, Severity } from "./fill/notification";
|
||||||
|
|
||||||
interface IRetryItem {
|
interface IRetryItem {
|
||||||
@ -105,7 +105,7 @@ export class Retry {
|
|||||||
/**
|
/**
|
||||||
* Retry a service.
|
* Retry a service.
|
||||||
*/
|
*/
|
||||||
public run(name: string): void {
|
public run(name: string, error?: Error): void {
|
||||||
if (!this.items.has(name)) {
|
if (!this.items.has(name)) {
|
||||||
throw new Error(`"${name}" is not registered`);
|
throw new Error(`"${name}" is not registered`);
|
||||||
}
|
}
|
||||||
@ -117,7 +117,7 @@ export class Retry {
|
|||||||
|
|
||||||
item.running = true;
|
item.running = true;
|
||||||
// This timeout is for the case when the connection drops; this allows time
|
// This timeout is for the case when the connection drops; this allows time
|
||||||
// for the Wush service to come in and block everything because some other
|
// for the socket service to come in and block everything because some other
|
||||||
// services might make it here first and try to restart, which will fail.
|
// services might make it here first and try to restart, which will fail.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.blocked && this.blocked !== name) {
|
if (this.blocked && this.blocked !== name) {
|
||||||
@ -125,7 +125,7 @@ export class Retry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!item.count || item.count < this.maxImmediateRetries) {
|
if (!item.count || item.count < this.maxImmediateRetries) {
|
||||||
return this.runItem(name, item);
|
return this.runItem(name, item, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!item.delay) {
|
if (!item.delay) {
|
||||||
@ -137,10 +137,10 @@ export class Retry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Retrying ${name.toLowerCase()} in ${item.delay}s`);
|
logger.info(`Retrying ${name.toLowerCase()} in ${item.delay}s`, error && field("error", error.message));
|
||||||
const itemDelayMs = item.delay * 1000;
|
const itemDelayMs = item.delay * 1000;
|
||||||
item.end = Date.now() + itemDelayMs;
|
item.end = Date.now() + itemDelayMs;
|
||||||
item.timeout = setTimeout(() => this.runItem(name, item), itemDelayMs);
|
item.timeout = setTimeout(() => this.runItem(name, item, error), itemDelayMs);
|
||||||
|
|
||||||
this.updateNotification();
|
this.updateNotification();
|
||||||
}, this.waitDelay);
|
}, this.waitDelay);
|
||||||
@ -165,7 +165,7 @@ export class Retry {
|
|||||||
/**
|
/**
|
||||||
* Run an item.
|
* Run an item.
|
||||||
*/
|
*/
|
||||||
private runItem(name: string, item: IRetryItem): void {
|
private runItem(name: string, item: IRetryItem, error?: Error): void {
|
||||||
if (!item.count) {
|
if (!item.count) {
|
||||||
item.count = 1;
|
item.count = 1;
|
||||||
} else {
|
} else {
|
||||||
@ -175,7 +175,7 @@ export class Retry {
|
|||||||
const retryCountText = item.count <= this.maxImmediateRetries
|
const retryCountText = item.count <= this.maxImmediateRetries
|
||||||
? `[${item.count}/${this.maxImmediateRetries}]`
|
? `[${item.count}/${this.maxImmediateRetries}]`
|
||||||
: `[${item.count}]`;
|
: `[${item.count}]`;
|
||||||
logger.info(`Trying ${name.toLowerCase()} ${retryCountText}...`);
|
logger.info(`Starting ${name.toLowerCase()} ${retryCountText}...`, error && field("error", error.message));
|
||||||
|
|
||||||
const endItem = (): void => {
|
const endItem = (): void => {
|
||||||
this.stopItem(item);
|
this.stopItem(item);
|
||||||
|
@ -38,8 +38,10 @@ export class Time {
|
|||||||
) { }
|
) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `undefined` is allowed to make it easy to conditionally display a field.
|
||||||
|
// For example: `error && field("error", error)`
|
||||||
// tslint:disable-next-line no-any
|
// tslint:disable-next-line no-any
|
||||||
export type FieldArray = Array<Field<any>>;
|
export type FieldArray = Array<Field<any> | undefined>;
|
||||||
|
|
||||||
// Functions can be used to remove the need to perform operations when the
|
// Functions can be used to remove the need to perform operations when the
|
||||||
// logging level won't output the result anyway.
|
// logging level won't output the result anyway.
|
||||||
@ -338,9 +340,9 @@ export class Logger {
|
|||||||
passedFields = values as FieldArray;
|
passedFields = values as FieldArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = this.defaultFields
|
const fields = (this.defaultFields
|
||||||
? passedFields.concat(this.defaultFields)
|
? passedFields.filter((f) => !!f).concat(this.defaultFields)
|
||||||
: passedFields;
|
: passedFields.filter((f) => !!f)) as Array<Field<any>>; // tslint:disable-line no-any
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let times: Array<Field<Time>> = [];
|
let times: Array<Field<Time>> = [];
|
||||||
|
@ -92,7 +92,6 @@ export class Entry extends Command {
|
|||||||
logger.info("Additional documentation: https://coder.com/docs");
|
logger.info("Additional documentation: https://coder.com/docs");
|
||||||
logger.info("Initializing", field("data-dir", dataDir), field("working-dir", workingDir), field("log-dir", logDir));
|
logger.info("Initializing", field("data-dir", dataDir), field("working-dir", workingDir), field("log-dir", logDir));
|
||||||
const sharedProcess = new SharedProcess(dataDir, builtInExtensionsDir);
|
const sharedProcess = new SharedProcess(dataDir, builtInExtensionsDir);
|
||||||
logger.info("Starting shared process...", field("socket", sharedProcess.socketPath));
|
|
||||||
const sendSharedProcessReady = (socket: WebSocket): void => {
|
const sendSharedProcessReady = (socket: WebSocket): void => {
|
||||||
const active = new SharedProcessActiveMessage();
|
const active = new SharedProcessActiveMessage();
|
||||||
active.setSocketPath(sharedProcess.socketPath);
|
active.setSocketPath(sharedProcess.socketPath);
|
||||||
@ -102,12 +101,7 @@ export class Entry extends Command {
|
|||||||
socket.send(serverMessage.serializeBinary());
|
socket.send(serverMessage.serializeBinary());
|
||||||
};
|
};
|
||||||
sharedProcess.onState((event) => {
|
sharedProcess.onState((event) => {
|
||||||
if (event.state === SharedProcessState.Stopped) {
|
if (event.state === SharedProcessState.Ready) {
|
||||||
logger.error("Shared process stopped. Restarting...", field("error", event.error));
|
|
||||||
} else if (event.state === SharedProcessState.Starting) {
|
|
||||||
logger.info("Starting shared process...");
|
|
||||||
} else if (event.state === SharedProcessState.Ready) {
|
|
||||||
logger.info("Shared process is ready!");
|
|
||||||
app.wss.clients.forEach((c) => sendSharedProcessReady(c));
|
app.wss.clients.forEach((c) => sendSharedProcessReady(c));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,8 @@ import { forkModule } from "./bootstrapFork";
|
|||||||
import { StdioIpcHandler } from "../ipc";
|
import { StdioIpcHandler } from "../ipc";
|
||||||
import { ParsedArgs } from "vs/platform/environment/common/environment";
|
import { ParsedArgs } from "vs/platform/environment/common/environment";
|
||||||
import { LogLevel } from "vs/platform/log/common/log";
|
import { LogLevel } from "vs/platform/log/common/log";
|
||||||
import { Emitter, Event } from "@coder/events/src";
|
import { Emitter } from "@coder/events/src";
|
||||||
|
import { retry } from "@coder/ide/src/retry";
|
||||||
|
|
||||||
export enum SharedProcessState {
|
export enum SharedProcessState {
|
||||||
Stopped,
|
Stopped,
|
||||||
@ -28,12 +29,14 @@ export class SharedProcess {
|
|||||||
private ipcHandler: StdioIpcHandler | undefined;
|
private ipcHandler: StdioIpcHandler | undefined;
|
||||||
private readonly onStateEmitter = new Emitter<SharedProcessEvent>();
|
private readonly onStateEmitter = new Emitter<SharedProcessEvent>();
|
||||||
public readonly onState = this.onStateEmitter.event;
|
public readonly onState = this.onStateEmitter.event;
|
||||||
|
private readonly retryName = "Shared process";
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly userDataDir: string,
|
private readonly userDataDir: string,
|
||||||
private readonly builtInExtensionsDir: string,
|
private readonly builtInExtensionsDir: string,
|
||||||
) {
|
) {
|
||||||
this.restart();
|
retry.register(this.retryName, () => this.restart());
|
||||||
|
retry.run(this.retryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get state(): SharedProcessState {
|
public get state(): SharedProcessState {
|
||||||
@ -73,7 +76,7 @@ export class SharedProcess {
|
|||||||
state: SharedProcessState.Stopped,
|
state: SharedProcessState.Stopped,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.restart();
|
retry.run(this.retryName, new Error(`Exited with ${err}`));
|
||||||
});
|
});
|
||||||
this.ipcHandler = new StdioIpcHandler(this.activeProcess);
|
this.ipcHandler = new StdioIpcHandler(this.activeProcess);
|
||||||
this.ipcHandler.once("handshake:hello", () => {
|
this.ipcHandler.once("handshake:hello", () => {
|
||||||
@ -94,6 +97,7 @@ export class SharedProcess {
|
|||||||
});
|
});
|
||||||
this.ipcHandler.once("handshake:im ready", () => {
|
this.ipcHandler.once("handshake:im ready", () => {
|
||||||
resolved = true;
|
resolved = true;
|
||||||
|
retry.recover(this.retryName);
|
||||||
this.setState({
|
this.setState({
|
||||||
state: SharedProcessState.Ready,
|
state: SharedProcessState.Ready,
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user