Wrap shared process in retry

This commit is contained in:
Asher 2019-02-06 18:11:31 -06:00
parent 5d02194048
commit 499798fc17
No known key found for this signature in database
GPG Key ID: 7BB4BA9C783D2BBC
5 changed files with 36 additions and 26 deletions

View File

@ -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> {

View File

@ -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);

View File

@ -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>> = [];

View File

@ -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));
} }
}); });

View File

@ -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,
}); });