Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] add timeout for "stop" lifecycle #90432

Merged
merged 6 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions packages/kbn-std/src/promise.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,36 @@ const delay = (ms: number, resolveValue?: any) =>
new Promise((resolve) => setTimeout(resolve, ms, resolveValue));

describe('withTimeout', () => {
it('resolves with a promise value if resolved in given timeout', async () => {
it('resolves with a promise value and "timedout: false" if resolved in given timeout', async () => {
await expect(
withTimeout({
promise: delay(10, 'value'),
timeout: 200,
errorMessage: 'error-message',
timeoutMs: 200,
})
).resolves.toBe('value');
).resolves.toStrictEqual({ value: 'value', timedout: false });
});

it('rejects with errorMessage if not resolved in given time', async () => {
it('resolves with "timedout: false" if not resolved in given time', async () => {
await expect(
withTimeout({
promise: delay(200, 'value'),
timeout: 10,
errorMessage: 'error-message',
timeoutMs: 10,
})
).rejects.toMatchInlineSnapshot(`[Error: error-message]`);
).resolves.toStrictEqual({ timedout: true });

await expect(
withTimeout({
promise: new Promise((i) => i),
timeout: 10,
errorMessage: 'error-message',
timeoutMs: 10,
})
).rejects.toMatchInlineSnapshot(`[Error: error-message]`);
).resolves.toStrictEqual({ timedout: true });
});

it('does not swallow promise error', async () => {
await expect(
withTimeout({
promise: Promise.reject(new Error('from-promise')),
timeout: 10,
errorMessage: 'error-message',
timeoutMs: 10,
})
).rejects.toMatchInlineSnapshot(`[Error: from-promise]`);
});
Expand Down
27 changes: 17 additions & 10 deletions packages/kbn-std/src/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@
* Side Public License, v 1.
*/

export function withTimeout<T>({
export async function withTimeout<T>({
promise,
timeout,
errorMessage,
timeoutMs,
}: {
promise: Promise<T>;
timeout: number;
errorMessage: string;
}) {
return Promise.race([
promise,
new Promise((resolve, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout)),
]) as Promise<T>;
timeoutMs: number;
}): Promise<{ timedout: true } | { timedout: false; value: T }> {
let timeout: NodeJS.Timeout | undefined;
try {
return (await Promise.race([
promise.then((v) => ({ value: v, timedout: false })),
new Promise((resolve) => {
timeout = setTimeout(() => resolve({ timedout: true }), timeoutMs);
}),
])) as Promise<{ timedout: true } | { timedout: false; value: T }>;
} finally {
if (timeout !== undefined) {
clearTimeout(timeout);
}
}
}

export function isPromise<T>(maybePromise: T | Promise<T>): maybePromise is Promise<T> {
Expand Down
26 changes: 20 additions & 6 deletions src/core/public/plugins/plugins_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,18 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
);
}

contract = await withTimeout({
const contractMaybe = await withTimeout({
promise: contractOrPromise,
timeout: 10 * Sec,
errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`,
timeoutMs: 10 * Sec,
});

if (contractMaybe.timedout) {
throw new Error(
`Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`
);
} else {
contract = contractMaybe.value;
}
} else {
contract = contractOrPromise;
}
Expand Down Expand Up @@ -158,11 +165,18 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
);
}

contract = await withTimeout({
const contractMaybe = await withTimeout({
promise: contractOrPromise,
timeout: 10 * Sec,
errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`,
timeoutMs: 10 * Sec,
});

if (contractMaybe.timedout) {
throw new Error(
`Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`
);
} else {
contract = contractMaybe.value;
}
} else {
contract = contractOrPromise;
}
Expand Down
42 changes: 42 additions & 0 deletions src/core/server/plugins/plugins_system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,45 @@ describe('asynchronous plugins', () => {
`);
});
});

describe('stop', () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

it('waits for 30 sec to finish "stop" and move on to the next plugin.', async () => {
const [plugin1, plugin2] = [createPlugin('timeout-stop-1'), createPlugin('timeout-stop-2')].map(
(plugin, index) => {
jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`);
jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`);
pluginsSystem.addPlugin(plugin);
return plugin;
}
);

const stopSpy1 = jest
.spyOn(plugin1, 'stop')
.mockImplementationOnce(() => new Promise((resolve) => resolve));
const stopSpy2 = jest.spyOn(plugin2, 'stop').mockImplementationOnce(() => Promise.resolve());

mockCreatePluginSetupContext.mockImplementation(() => ({}));

await pluginsSystem.setupPlugins(setupDeps);
const stopPromise = pluginsSystem.stopPlugins();

jest.runAllTimers();
await stopPromise;
expect(stopSpy1).toHaveBeenCalledTimes(1);
expect(stopSpy2).toHaveBeenCalledTimes(1);

expect(loggingSystemMock.collect(logger).warn.flat()).toEqual(
expect.arrayContaining([
`"timeout-stop-1" plugin didn't stop in 30sec., move on to the next.`,
])
);
});
});
36 changes: 29 additions & 7 deletions src/core/server/plugins/plugins_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,18 @@ export class PluginsSystem {
`Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.`
);
}
contract = await withTimeout({
const contractMaybe = await withTimeout<any>({
promise: contractOrPromise,
timeout: 10 * Sec,
errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`,
timeoutMs: 10 * Sec,
});

if (contractMaybe.timedout) {
throw new Error(
`Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`
);
} else {
contract = contractMaybe.value;
}
} else {
contract = contractOrPromise;
}
Expand Down Expand Up @@ -154,11 +161,18 @@ export class PluginsSystem {
`Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.`
);
}
contract = await withTimeout({
const contractMaybe = await withTimeout({
promise: contractOrPromise,
timeout: 10 * Sec,
errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`,
timeoutMs: 10 * Sec,
});

if (contractMaybe.timedout) {
throw new Error(
`Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`
);
} else {
contract = contractMaybe.value;
}
} else {
contract = contractOrPromise;
}
Expand All @@ -181,7 +195,15 @@ export class PluginsSystem {
const pluginName = this.satupPlugins.pop()!;

this.log.debug(`Stopping plugin "${pluginName}"...`);
await this.plugins.get(pluginName)!.stop();

const resultMaybe = await withTimeout({
promise: this.plugins.get(pluginName)!.stop(),
timeoutMs: 30 * Sec,
});

if (resultMaybe?.timedout) {
this.log.warn(`"${pluginName}" plugin didn't stop in 30sec., move on to the next.`);
}
}
}

Expand Down