mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[7.17] [ftr] handle unexpected Kibana/ES shutdowns better (#131768)
This commit is contained in:
parent
336e643480
commit
bbe719d692
11 changed files with 107 additions and 28 deletions
|
@ -168,5 +168,8 @@ export function startProc(name: string, options: ProcOptions, log: ToolingLog) {
|
|||
outcome$,
|
||||
outcomePromise,
|
||||
stop,
|
||||
stopWasCalled() {
|
||||
return stopCalled;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ const noop = () => {};
|
|||
interface RunOptions extends ProcOptions {
|
||||
wait: true | RegExp;
|
||||
waitTimeout?: number | false;
|
||||
onEarlyExit?: (msg: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,16 +49,6 @@ export class ProcRunner {
|
|||
|
||||
/**
|
||||
* Start a process, tracking it by `name`
|
||||
* @param {String} name
|
||||
* @param {Object} options
|
||||
* @property {String} options.cmd executable to run
|
||||
* @property {Array<String>?} options.args arguments to provide the executable
|
||||
* @property {String?} options.cwd current working directory for the process
|
||||
* @property {RegExp|Boolean} options.wait Should start() wait for some time? Use
|
||||
* `true` will wait until the proc exits,
|
||||
* a `RegExp` will wait until that log line
|
||||
* is found
|
||||
* @return {Promise<undefined>}
|
||||
*/
|
||||
async run(name: string, options: RunOptions) {
|
||||
const {
|
||||
|
@ -67,6 +58,7 @@ export class ProcRunner {
|
|||
wait = false,
|
||||
waitTimeout = 15 * MINUTE,
|
||||
env = process.env,
|
||||
onEarlyExit,
|
||||
} = options;
|
||||
const cmd = options.cmd === 'node' ? process.execPath : options.cmd;
|
||||
|
||||
|
@ -90,6 +82,25 @@ export class ProcRunner {
|
|||
stdin,
|
||||
});
|
||||
|
||||
if (onEarlyExit) {
|
||||
proc.outcomePromise
|
||||
.then(
|
||||
(code) => {
|
||||
if (!proc.stopWasCalled()) {
|
||||
onEarlyExit(`[${name}] exitted early with ${code}`);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!proc.stopWasCalled()) {
|
||||
onEarlyExit(`[${name}] exitted early: ${error.message}`);
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
throw new Error(`Error handling early exit: ${error.stack}`);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (wait instanceof RegExp) {
|
||||
// wait for process to log matching line
|
||||
|
|
|
@ -181,6 +181,25 @@ exports.Cluster = class Cluster {
|
|||
throw createCliError('ES exited without starting');
|
||||
}),
|
||||
]);
|
||||
|
||||
if (options.onEarlyExit) {
|
||||
this._outcome
|
||||
.then(
|
||||
() => {
|
||||
if (!this._stopCalled) {
|
||||
options.onEarlyExit(`ES exitted unexpectedly`);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!this._stopCalled) {
|
||||
options.onEarlyExit(`ES exitted unexpectedly: ${error.stack}`);
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
throw new Error(`failure handling early exit: ${error.stack}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -142,6 +142,11 @@ export interface CreateTestEsClusterOptions {
|
|||
*/
|
||||
port?: number;
|
||||
ssl?: boolean;
|
||||
/**
|
||||
* Report to the creator of the es-test-cluster that the es node has exitted before stop() was called, allowing
|
||||
* this caller to react appropriately. If this is not passed then an uncatchable exception will be thrown
|
||||
*/
|
||||
onEarlyExit?: (msg: string) => void;
|
||||
}
|
||||
|
||||
export function createTestEsCluster<
|
||||
|
@ -161,6 +166,7 @@ export function createTestEsCluster<
|
|||
esJavaOpts,
|
||||
clusterName: customClusterName = 'es-test-cluster',
|
||||
ssl,
|
||||
onEarlyExit,
|
||||
} = options;
|
||||
|
||||
const clusterName = `${CI_PARALLEL_PROCESS_PREFIX}${customClusterName}`;
|
||||
|
@ -254,6 +260,7 @@ export function createTestEsCluster<
|
|||
// right away, or ES will complain as the cluster isn't ready. So we only
|
||||
// set it up after the last node is started.
|
||||
skipNativeRealmSetup: this.nodes.length > 1 && i < this.nodes.length - 1,
|
||||
onEarlyExit,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ export interface Test {
|
|||
export interface Runner extends EventEmitter {
|
||||
abort(): void;
|
||||
failures: any[];
|
||||
uncaught: (error: Error) => void;
|
||||
}
|
||||
|
||||
export interface Mocha {
|
||||
|
|
|
@ -54,7 +54,7 @@ export class FunctionalTestRunner {
|
|||
: new EsVersion(esVersion);
|
||||
}
|
||||
|
||||
async run() {
|
||||
async run(abortSignal?: AbortSignal) {
|
||||
return await this._run(async (config, coreProviders) => {
|
||||
SuiteTracker.startTracking(this.lifecycle, this.configFile);
|
||||
|
||||
|
@ -98,6 +98,10 @@ export class FunctionalTestRunner {
|
|||
reporter,
|
||||
reporterOptions
|
||||
);
|
||||
if (abortSignal?.aborted) {
|
||||
this.log.warning('run aborted');
|
||||
return;
|
||||
}
|
||||
|
||||
// there's a bug in mocha's dry run, see https://github.com/mochajs/mocha/issues/4838
|
||||
// until we can update to a mocha version where this is fixed, we won't actually
|
||||
|
@ -109,9 +113,14 @@ export class FunctionalTestRunner {
|
|||
}
|
||||
|
||||
await this.lifecycle.beforeTests.trigger(mocha.suite);
|
||||
this.log.info('Starting tests');
|
||||
|
||||
return await runTests(this.lifecycle, mocha);
|
||||
if (abortSignal?.aborted) {
|
||||
this.log.warning('run aborted');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info('Starting tests');
|
||||
return await runTests(this.lifecycle, mocha, abortSignal);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Lifecycle } from '../lifecycle';
|
||||
import { Mocha } from '../../fake_mocha_types';
|
||||
|
||||
|
@ -18,14 +20,23 @@ import { Mocha } from '../../fake_mocha_types';
|
|||
* @param {Mocha} mocha
|
||||
* @return {Promise<Number>} resolves to the number of test failures
|
||||
*/
|
||||
export async function runTests(lifecycle: Lifecycle, mocha: Mocha) {
|
||||
export async function runTests(lifecycle: Lifecycle, mocha: Mocha, abortSignal?: AbortSignal) {
|
||||
let runComplete = false;
|
||||
const runner = mocha.run(() => {
|
||||
runComplete = true;
|
||||
});
|
||||
|
||||
lifecycle.cleanup.add(() => {
|
||||
if (!runComplete) runner.abort();
|
||||
Rx.race(
|
||||
lifecycle.cleanup.before$,
|
||||
abortSignal ? Rx.fromEvent(abortSignal, 'abort').pipe(take(1)) : Rx.NEVER
|
||||
).subscribe({
|
||||
next() {
|
||||
if (!runComplete) {
|
||||
runComplete = true;
|
||||
runner.uncaught(new Error('Forcing mocha to abort'));
|
||||
runner.abort();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
|
|
@ -19,9 +19,11 @@ interface RunElasticsearchOptions {
|
|||
export async function runElasticsearch({
|
||||
config,
|
||||
options,
|
||||
onEarlyExit,
|
||||
}: {
|
||||
config: Config;
|
||||
options: RunElasticsearchOptions;
|
||||
onEarlyExit?: (msg: string) => void;
|
||||
}) {
|
||||
const { log, esFrom } = options;
|
||||
const ssl = config.get('esTestCluster.ssl');
|
||||
|
@ -41,6 +43,7 @@ export async function runElasticsearch({
|
|||
esArgs,
|
||||
esJavaOpts,
|
||||
ssl,
|
||||
onEarlyExit,
|
||||
});
|
||||
|
||||
await cluster.start();
|
||||
|
|
|
@ -81,8 +81,8 @@ async function createFtr({
|
|||
};
|
||||
}
|
||||
|
||||
export async function assertNoneExcluded({ configPath, options }: CreateFtrParams) {
|
||||
const { config, ftr } = await createFtr({ configPath, options });
|
||||
export async function assertNoneExcluded(params: CreateFtrParams) {
|
||||
const { config, ftr } = await createFtr(params);
|
||||
|
||||
if (config.get('testRunner')) {
|
||||
// tests with custom test runners are not included in this check
|
||||
|
@ -92,21 +92,21 @@ export async function assertNoneExcluded({ configPath, options }: CreateFtrParam
|
|||
const stats = await ftr.getTestStats();
|
||||
if (stats.testsExcludedByTag.length > 0) {
|
||||
throw new CliError(`
|
||||
${stats.testsExcludedByTag.length} tests in the ${configPath} config
|
||||
${stats.testsExcludedByTag.length} tests in the ${params.configPath} config
|
||||
are excluded when filtering by the tags run on CI. Make sure that all suites are
|
||||
tagged with one of the following tags:
|
||||
|
||||
${JSON.stringify(options.suiteTags)}
|
||||
${JSON.stringify(params.options.suiteTags)}
|
||||
|
||||
- ${stats.testsExcludedByTag.join('\n - ')}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runFtr({ configPath, options }: CreateFtrParams) {
|
||||
const { ftr } = await createFtr({ configPath, options });
|
||||
export async function runFtr(params: CreateFtrParams, signal?: AbortSignal) {
|
||||
const { ftr } = await createFtr(params);
|
||||
|
||||
const failureCount = await ftr.run();
|
||||
const failureCount = await ftr.run(signal);
|
||||
if (failureCount > 0) {
|
||||
throw new CliError(
|
||||
`${failureCount} functional test ${failureCount === 1 ? 'failure' : 'failures'}`
|
||||
|
@ -114,8 +114,8 @@ export async function runFtr({ configPath, options }: CreateFtrParams) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function hasTests({ configPath, options }: CreateFtrParams) {
|
||||
const { ftr, config } = await createFtr({ configPath, options });
|
||||
export async function hasTests(params: CreateFtrParams) {
|
||||
const { ftr, config } = await createFtr(params);
|
||||
|
||||
if (config.get('testRunner')) {
|
||||
// configs with custom test runners are assumed to always have tests
|
||||
|
|
|
@ -31,10 +31,12 @@ export async function runKibanaServer({
|
|||
procs,
|
||||
config,
|
||||
options,
|
||||
onEarlyExit,
|
||||
}: {
|
||||
procs: ProcRunner;
|
||||
config: Config;
|
||||
options: { installDir?: string; extraKbnOpts?: string[] };
|
||||
onEarlyExit?: (msg: string) => void;
|
||||
}) {
|
||||
const { installDir } = options;
|
||||
const runOptions = config.get('kbnTestServer.runOptions');
|
||||
|
@ -51,6 +53,7 @@ export async function runKibanaServer({
|
|||
},
|
||||
cwd: installDir || KIBANA_ROOT,
|
||||
wait: runOptions.wait,
|
||||
onEarlyExit,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -106,14 +106,26 @@ export async function runTests(options: RunTestsParams) {
|
|||
|
||||
await withProcRunner(log, async (procs) => {
|
||||
const config = await readConfigFile(log, options.esVersion, configPath);
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const onEarlyExit = (msg: string) => {
|
||||
log.error(msg);
|
||||
abortCtrl.abort();
|
||||
};
|
||||
|
||||
let es;
|
||||
try {
|
||||
if (process.env.TEST_ES_DISABLE_STARTUP !== 'true') {
|
||||
es = await runElasticsearch({ config, options: { ...options, log } });
|
||||
es = await runElasticsearch({ config, options: { ...options, log }, onEarlyExit });
|
||||
if (abortCtrl.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await runKibanaServer({ procs, config, options });
|
||||
await runFtr({ configPath, options: { ...options, log } });
|
||||
await runKibanaServer({ procs, config, options, onEarlyExit });
|
||||
if (abortCtrl.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
await runFtr({ configPath, options: { ...options, log } }, abortCtrl.signal);
|
||||
} finally {
|
||||
try {
|
||||
const delay = config.get('kbnTestServer.delayShutdown');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue