[7.17] [ftr] handle unexpected Kibana/ES shutdowns better (#131768)

This commit is contained in:
Spencer 2022-05-09 12:05:59 -05:00 committed by GitHub
parent 336e643480
commit bbe719d692
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 107 additions and 28 deletions

View file

@ -168,5 +168,8 @@ export function startProc(name: string, options: ProcOptions, log: ToolingLog) {
outcome$,
outcomePromise,
stop,
stopWasCalled() {
return stopCalled;
},
};
}

View file

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

View file

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

View file

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

View file

@ -37,6 +37,7 @@ export interface Test {
export interface Runner extends EventEmitter {
abort(): void;
failures: any[];
uncaught: (error: Error) => void;
}
export interface Mocha {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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