[7.x] [eslint] prevent async Promise constructor mistakes (#110349) (#110727)

* [eslint] prevent async Promise constructor mistakes (#110349)

Co-authored-by: spalger <spalger@users.noreply.github.com>

* autofix one more violation

Co-authored-by: Spencer <email@spalger.com>
Co-authored-by: spalger <spalger@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-09-01 21:18:23 -04:00 committed by GitHub
parent 0824bd8c92
commit 2ee5e66b26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1420 additions and 786 deletions

View file

@ -657,6 +657,7 @@
"@types/yauzl": "^2.9.1",
"@types/zen-observable": "^0.8.0",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/typescript-estree": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"@yarnpkg/lockfile": "^1.1.0",
"abab": "^2.0.4",
@ -727,6 +728,7 @@
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-react-perf": "^3.2.3",
"eslint-traverse": "^1.0.0",
"expose-loader": "^0.7.5",
"faker": "^5.1.0",
"fancy-log": "^1.3.2",

View file

@ -90,5 +90,7 @@ module.exports = {
},
],
],
'@kbn/eslint/no_async_promise_body': 'error',
},
};

View file

@ -12,5 +12,6 @@ module.exports = {
'disallow-license-headers': require('./rules/disallow_license_headers'),
'no-restricted-paths': require('./rules/no_restricted_paths'),
module_migration: require('./rules/module_migration'),
no_async_promise_body: require('./rules/no_async_promise_body'),
},
};

View file

@ -0,0 +1,165 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
const { parseExpression } = require('@babel/parser');
const { default: generate } = require('@babel/generator');
const tsEstree = require('@typescript-eslint/typescript-estree');
const traverse = require('eslint-traverse');
const esTypes = tsEstree.AST_NODE_TYPES;
const babelTypes = require('@babel/types');
/** @typedef {import("eslint").Rule.RuleModule} Rule */
/** @typedef {import("@typescript-eslint/parser").ParserServices} ParserServices */
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.Expression} Expression */
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.ArrowFunctionExpression} ArrowFunctionExpression */
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.FunctionExpression} FunctionExpression */
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.TryStatement} TryStatement */
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.NewExpression} NewExpression */
/** @typedef {import("typescript").ExportDeclaration} ExportDeclaration */
/** @typedef {import("eslint").Rule.RuleFixer} Fixer */
const ERROR_MSG =
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections';
/**
* @param {Expression} node
*/
const isPromise = (node) => node.type === esTypes.Identifier && node.name === 'Promise';
/**
* @param {Expression} node
* @returns {node is ArrowFunctionExpression | FunctionExpression}
*/
const isFunc = (node) =>
node.type === esTypes.ArrowFunctionExpression || node.type === esTypes.FunctionExpression;
/**
* @param {any} context
* @param {ArrowFunctionExpression | FunctionExpression} node
*/
const isFuncBodySafe = (context, node) => {
// if the body isn't wrapped in a blockStatement it can't have a try/catch at the root
if (node.body.type !== esTypes.BlockStatement) {
return false;
}
// when the entire body is wrapped in a try/catch it is the only node
if (node.body.body.length !== 1) {
return false;
}
const tryNode = node.body.body[0];
// ensure we have a try node with a handler
if (tryNode.type !== esTypes.TryStatement || !tryNode.handler) {
return false;
}
// ensure the handler doesn't throw
let hasThrow = false;
traverse(context, tryNode.handler, (path) => {
if (path.node.type === esTypes.ThrowStatement) {
hasThrow = true;
return traverse.STOP;
}
});
return !hasThrow;
};
/**
* @param {string} code
*/
const wrapFunctionInTryCatch = (code) => {
// parse the code with babel so we can mutate the AST
const ast = parseExpression(code, {
plugins: ['typescript', 'jsx'],
});
// validate that the code reperesents an arrow or function expression
if (!babelTypes.isArrowFunctionExpression(ast) && !babelTypes.isFunctionExpression(ast)) {
throw new Error('expected function to be an arrow or function expression');
}
// ensure that the function receives the second argument, and capture its name if already defined
let rejectName = 'reject';
if (ast.params.length === 0) {
ast.params.push(babelTypes.identifier('resolve'), babelTypes.identifier(rejectName));
} else if (ast.params.length === 1) {
ast.params.push(babelTypes.identifier(rejectName));
} else if (ast.params.length === 2) {
if (babelTypes.isIdentifier(ast.params[1])) {
rejectName = ast.params[1].name;
} else {
throw new Error('expected second param of promise definition function to be an identifier');
}
}
// ensure that the body of the function is a blockStatement
let block = ast.body;
if (!babelTypes.isBlockStatement(block)) {
block = babelTypes.blockStatement([babelTypes.returnStatement(block)]);
}
// redefine the body of the function as a new blockStatement containing a tryStatement
// which catches errors and forwards them to reject() when caught
ast.body = babelTypes.blockStatement([
// try {
babelTypes.tryStatement(
block,
// catch (error) {
babelTypes.catchClause(
babelTypes.identifier('error'),
babelTypes.blockStatement([
// reject(error)
babelTypes.expressionStatement(
babelTypes.callExpression(babelTypes.identifier(rejectName), [
babelTypes.identifier('error'),
])
),
])
)
),
]);
return generate(ast).code;
};
/** @type {Rule} */
module.exports = {
meta: {
fixable: 'code',
schema: [],
},
create: (context) => ({
NewExpression(_) {
const node = /** @type {NewExpression} */ (_);
// ensure we are newing up a promise with a single argument
if (!isPromise(node.callee) || node.arguments.length !== 1) {
return;
}
const func = node.arguments[0];
// ensure the argument is an arrow or function expression and is async
if (!isFunc(func) || !func.async) {
return;
}
// body must be a blockStatement, try/catch can't exist outside of a block
if (!isFuncBodySafe(context, func)) {
context.report({
message: ERROR_MSG,
loc: func.loc,
fix(fixer) {
const source = context.getSourceCode();
return fixer.replaceText(func, wrapFunctionInTryCatch(source.getText(func)));
},
});
}
},
}),
};

View file

@ -0,0 +1,254 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
const { RuleTester } = require('eslint');
const rule = require('./no_async_promise_body');
const dedent = require('dedent');
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
ecmaFeatures: {
jsx: true,
},
},
});
ruleTester.run('@kbn/eslint/no_async_promise_body', rule, {
valid: [
// caught but no resolve
{
code: dedent`
new Promise(async function (resolve) {
try {
await asyncOperation();
} catch (error) {
// noop
}
})
`,
},
// arrow caught but no resolve
{
code: dedent`
new Promise(async (resolve) => {
try {
await asyncOperation();
} catch (error) {
// noop
}
})
`,
},
// caught with reject
{
code: dedent`
new Promise(async function (resolve, reject) {
try {
await asyncOperation();
} catch (error) {
reject(error)
}
})
`,
},
// arrow caught with reject
{
code: dedent`
new Promise(async (resolve, reject) => {
try {
await asyncOperation();
} catch (error) {
reject(error)
}
})
`,
},
// non async
{
code: dedent`
new Promise(function (resolve) {
setTimeout(resolve, 10);
})
`,
},
// arrow non async
{
code: dedent`
new Promise((resolve) => setTimeout(resolve, 10))
`,
},
],
invalid: [
// no catch
{
code: dedent`
new Promise(async function (resolve) {
const result = await asyncOperation();
resolve(result);
})
`,
errors: [
{
line: 1,
message:
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
},
],
output: dedent`
new Promise(async function (resolve, reject) {
try {
const result = await asyncOperation();
resolve(result);
} catch (error) {
reject(error);
}
})
`,
},
// arrow no catch
{
code: dedent`
new Promise(async (resolve) => {
const result = await asyncOperation();
resolve(result);
})
`,
errors: [
{
line: 1,
message:
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
},
],
output: dedent`
new Promise(async (resolve, reject) => {
try {
const result = await asyncOperation();
resolve(result);
} catch (error) {
reject(error);
}
})
`,
},
// catch, but it throws
{
code: dedent`
new Promise(async function (resolve) {
try {
const result = await asyncOperation();
resolve(result);
} catch (error) {
if (error.code === 'foo') {
throw error;
}
}
})
`,
errors: [
{
line: 1,
message:
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
},
],
output: dedent`
new Promise(async function (resolve, reject) {
try {
try {
const result = await asyncOperation();
resolve(result);
} catch (error) {
if (error.code === 'foo') {
throw error;
}
}
} catch (error) {
reject(error);
}
})
`,
},
// no catch without block
{
code: dedent`
new Promise(async (resolve) => resolve(await asyncOperation()));
`,
errors: [
{
line: 1,
message:
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
},
],
output: dedent`
new Promise(async (resolve, reject) => {
try {
return resolve(await asyncOperation());
} catch (error) {
reject(error);
}
});
`,
},
// no catch with named reject
{
code: dedent`
new Promise(async (resolve, rej) => {
const result = await asyncOperation();
result ? resolve(true) : rej()
});
`,
errors: [
{
line: 1,
message:
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
},
],
output: dedent`
new Promise(async (resolve, rej) => {
try {
const result = await asyncOperation();
result ? resolve(true) : rej();
} catch (error) {
rej(error);
}
});
`,
},
// no catch with no args
{
code: dedent`
new Promise(async () => {
await asyncOperation();
});
`,
errors: [
{
line: 1,
message:
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
},
],
output: dedent`
new Promise(async (resolve, reject) => {
try {
await asyncOperation();
} catch (error) {
reject(error);
}
});
`,
},
],
});

View file

@ -21,13 +21,18 @@ export const createRenderer = (element: ReactElement | null): Renderer => {
const dom: Dom = element && mount(<I18nProvider>{element}</I18nProvider>);
return () =>
new Promise(async (resolve) => {
if (dom) {
await act(async () => {
dom.update();
});
new Promise(async (resolve, reject) => {
try {
if (dom) {
await act(async () => {
dom.update();
});
}
setImmediate(() => resolve(dom)); // flushes any pending promises
} catch (error) {
reject(error);
}
setImmediate(() => resolve(dom)); // flushes any pending promises
});
};

View file

@ -27,8 +27,12 @@ describe('AppContainer', () => {
});
const flushPromises = async () => {
await new Promise<void>(async (resolve) => {
setImmediate(() => resolve());
await new Promise<void>(async (resolve, reject) => {
try {
setImmediate(() => resolve());
} catch (error) {
reject(error);
}
});
};

View file

@ -26,13 +26,18 @@ describe('HeaderActionMenu', () => {
});
const refresh = () => {
new Promise(async (resolve) => {
if (component) {
act(() => {
component.update();
});
new Promise(async (resolve, reject) => {
try {
if (component) {
act(() => {
component.update();
});
}
setImmediate(() => resolve(component)); // flushes any pending promises
} catch (error) {
reject(error);
}
setImmediate(() => resolve(component)); // flushes any pending promises
});
};

View file

@ -25,42 +25,47 @@ describe.skip('useActiveCursor', () => {
events: Array<Partial<ActiveCursorPayload>>,
eventsTimeout = 1
) =>
new Promise(async (resolve) => {
const activeCursor = new ActiveCursor();
let allEventsExecuted = false;
activeCursor.setup();
dispatchExternalPointerEvent.mockImplementation((pointerEvent) => {
if (allEventsExecuted) {
resolve(pointerEvent);
}
});
renderHook(() =>
useActiveCursor(
activeCursor,
{
current: {
dispatchExternalPointerEvent: dispatchExternalPointerEvent as (
pointerEvent: PointerEvent
) => void,
},
} as RefObject<Chart>,
{ ...syncOption, debounce: syncOption.debounce ?? 1 }
)
);
for (const e of events) {
await new Promise((eventResolve) =>
setTimeout(() => {
if (e === events[events.length - 1]) {
allEventsExecuted = true;
}
activeCursor.activeCursor$!.next({ cursor, ...e });
eventResolve(null);
}, eventsTimeout)
new Promise(async (resolve, reject) => {
try {
const activeCursor = new ActiveCursor();
let allEventsExecuted = false;
activeCursor.setup();
dispatchExternalPointerEvent.mockImplementation((pointerEvent) => {
if (allEventsExecuted) {
resolve(pointerEvent);
}
});
renderHook(() =>
useActiveCursor(
activeCursor,
{
current: {
dispatchExternalPointerEvent: dispatchExternalPointerEvent as (
pointerEvent: PointerEvent
) => void,
},
} as RefObject<Chart>,
{ ...syncOption, debounce: syncOption.debounce ?? 1 }
)
);
for (const e of events) {
await new Promise((eventResolve) =>
setTimeout(() => {
if (e === events[events.length - 1]) {
allEventsExecuted = true;
}
activeCursor.activeCursor$!.next({
cursor,
...e,
});
eventResolve(null);
}, eventsTimeout)
);
}
} catch (error) {
reject(error);
}
});

View file

@ -19,13 +19,18 @@ describe('MountPointPortal', () => {
let dom: ReactWrapper;
const refresh = () => {
new Promise(async (resolve) => {
if (dom) {
act(() => {
dom.update();
});
new Promise(async (resolve, reject) => {
try {
if (dom) {
act(() => {
dom.update();
});
}
setImmediate(() => resolve(dom)); // flushes any pending promises
} catch (error) {
reject(error);
}
setImmediate(() => resolve(dom)); // flushes any pending promises
});
};

View file

@ -16,10 +16,14 @@ export async function getServiceSettings(): Promise<IServiceSettings> {
return loadPromise;
}
loadPromise = new Promise(async (resolve) => {
const { ServiceSettings } = await import('./lazy');
const config = getMapsEmsConfig();
resolve(new ServiceSettings(config, config.tilemap));
loadPromise = new Promise(async (resolve, reject) => {
try {
const { ServiceSettings } = await import('./lazy');
const config = getMapsEmsConfig();
resolve(new ServiceSettings(config, config.tilemap));
} catch (error) {
reject(error);
}
});
return loadPromise;
}

View file

@ -18,13 +18,16 @@ export async function lazyLoadMapsLegacyModules(): Promise<LazyLoadedMapsLegacyM
return loadModulesPromise;
}
loadModulesPromise = new Promise(async (resolve) => {
const { KibanaMap, L } = await import('./lazy');
resolve({
KibanaMap,
L,
});
loadModulesPromise = new Promise(async (resolve, reject) => {
try {
const { KibanaMap, L } = await import('./lazy');
resolve({
KibanaMap,
L,
});
} catch (error) {
reject(error);
}
});
return loadModulesPromise;
}

View file

@ -109,13 +109,18 @@ describe('TopNavMenu', () => {
let dom: ReactWrapper;
const refresh = () => {
new Promise(async (resolve) => {
if (dom) {
act(() => {
dom.update();
});
new Promise(async (resolve, reject) => {
try {
if (dom) {
act(() => {
dom.update();
});
}
setImmediate(() => resolve(dom)); // flushes any pending promises
} catch (error) {
reject(error);
}
setImmediate(() => resolve(dom)); // flushes any pending promises
});
};

View file

@ -71,35 +71,42 @@ export function getTableVisualizationControllerClass(
await this.initLocalAngular();
return new Promise(async (resolve, reject) => {
if (!this.$rootScope) {
const $injector = this.getInjector();
this.$rootScope = $injector.get('$rootScope');
this.$compile = $injector.get('$compile');
}
const updateScope = () => {
if (!this.$scope) {
return;
try {
if (!this.$rootScope) {
const $injector = this.getInjector();
this.$rootScope = $injector.get('$rootScope');
this.$compile = $injector.get('$compile');
}
this.$scope.visState = { params: visParams, title: visParams.title };
this.$scope.esResponse = esResponse;
const updateScope = () => {
if (!this.$scope) {
return;
}
this.$scope.visParams = visParams;
this.$scope.renderComplete = resolve;
this.$scope.renderFailed = reject;
this.$scope.resize = Date.now();
this.$scope.$apply();
};
this.$scope.visState = {
params: visParams,
title: visParams.title,
};
this.$scope.esResponse = esResponse;
this.$scope.visParams = visParams;
this.$scope.renderComplete = resolve;
this.$scope.renderFailed = reject;
this.$scope.resize = Date.now();
this.$scope.$apply();
};
if (!this.$scope && this.$compile) {
this.$scope = this.$rootScope.$new();
this.$scope.uiState = handlers.uiState;
this.$scope.filter = handlers.event;
updateScope();
this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope));
this.$scope.$apply();
} else {
updateScope();
if (!this.$scope && this.$compile) {
this.$scope = this.$rootScope.$new();
this.$scope.uiState = handlers.uiState;
this.$scope.filter = handlers.event;
updateScope();
this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope));
this.$scope.$apply();
} else {
updateScope();
}
} catch (error) {
reject(error);
}
});
}

View file

@ -124,16 +124,25 @@ export class VisLegend extends PureComponent<VisLegendProps, VisLegendState> {
};
setFilterableLabels = (items: LegendItem[]): Promise<void> =>
new Promise(async (resolve) => {
const filterableLabels = new Set<string>();
items.forEach(async (item) => {
const canFilter = await this.canFilter(item);
if (canFilter) {
filterableLabels.add(item.label);
}
});
new Promise(async (resolve, reject) => {
try {
const filterableLabels = new Set<string>();
items.forEach(async (item) => {
const canFilter = await this.canFilter(item);
this.setState({ filterableLabels }, resolve);
if (canFilter) {
filterableLabels.add(item.label);
}
});
this.setState(
{
filterableLabels,
},
resolve
);
} catch (error) {
reject(error);
}
});
setLabels = (data: any, type: string) => {

View file

@ -22,13 +22,13 @@ export async function lazyLoadModules(): Promise<LazyLoadedModules> {
return loadModulesPromise;
}
loadModulesPromise = new Promise(async (resolve) => {
const lazyImports = await import('./lazy');
resolve({
...lazyImports,
getHttp: () => getCoreStart().http,
});
loadModulesPromise = new Promise(async (resolve, reject) => {
try {
const lazyImports = await import('./lazy');
resolve({ ...lazyImports, getHttp: () => getCoreStart().http });
} catch (error) {
reject(error);
}
});
return loadModulesPromise;
}

View file

@ -44,15 +44,18 @@ export async function lazyLoadModules(): Promise<LazyLoadedFileUploadModules> {
return loadModulesPromise;
}
loadModulesPromise = new Promise(async (resolve) => {
const { JsonUploadAndParse, importerFactory, IndexNameForm } = await import('./lazy');
resolve({
JsonUploadAndParse,
importerFactory,
getHttp,
IndexNameForm,
});
loadModulesPromise = new Promise(async (resolve, reject) => {
try {
const { JsonUploadAndParse, importerFactory, IndexNameForm } = await import('./lazy');
resolve({
JsonUploadAndParse,
importerFactory,
getHttp,
IndexNameForm,
});
} catch (error) {
reject(error);
}
});
return loadModulesPromise;
}

View file

@ -107,17 +107,25 @@ export const useMappingsStateListener = ({ onChange, value, mappingsType }: Args
validate: async () => {
const configurationFormValidator =
state.configuration.submitForm !== undefined
? new Promise(async (resolve) => {
const { isValid } = await state.configuration.submitForm!();
resolve(isValid);
? new Promise(async (resolve, reject) => {
try {
const { isValid } = await state.configuration.submitForm!();
resolve(isValid);
} catch (error) {
reject(error);
}
})
: Promise.resolve(true);
const templatesFormValidator =
state.templates.submitForm !== undefined
? new Promise(async (resolve) => {
const { isValid } = await state.templates.submitForm!();
resolve(isValid);
? new Promise(async (resolve, reject) => {
try {
const { isValid } = await state.templates.submitForm!();
resolve(isValid);
} catch (error) {
reject(error);
}
})
: Promise.resolve(true);

View file

@ -83,36 +83,39 @@ export async function lazyLoadMapModules(): Promise<LazyLoadedMapModules> {
return loadModulesPromise;
}
loadModulesPromise = new Promise(async (resolve) => {
const {
MapEmbeddable,
getIndexPatternService,
getMapsCapabilities,
renderApp,
createSecurityLayerDescriptors,
registerLayerWizard,
registerSource,
createTileMapLayerDescriptor,
createRegionMapLayerDescriptor,
createBasemapLayerDescriptor,
createESSearchSourceLayerDescriptor,
suggestEMSTermJoinConfig,
} = await import('./lazy');
resolve({
MapEmbeddable,
getIndexPatternService,
getMapsCapabilities,
renderApp,
createSecurityLayerDescriptors,
registerLayerWizard,
registerSource,
createTileMapLayerDescriptor,
createRegionMapLayerDescriptor,
createBasemapLayerDescriptor,
createESSearchSourceLayerDescriptor,
suggestEMSTermJoinConfig,
});
loadModulesPromise = new Promise(async (resolve, reject) => {
try {
const {
MapEmbeddable,
getIndexPatternService,
getMapsCapabilities,
renderApp,
createSecurityLayerDescriptors,
registerLayerWizard,
registerSource,
createTileMapLayerDescriptor,
createRegionMapLayerDescriptor,
createBasemapLayerDescriptor,
createESSearchSourceLayerDescriptor,
suggestEMSTermJoinConfig,
} = await import('./lazy');
resolve({
MapEmbeddable,
getIndexPatternService,
getMapsCapabilities,
renderApp,
createSecurityLayerDescriptors,
registerLayerWizard,
registerSource,
createTileMapLayerDescriptor,
createRegionMapLayerDescriptor,
createBasemapLayerDescriptor,
createESSearchSourceLayerDescriptor,
suggestEMSTermJoinConfig,
});
} catch (error) {
reject(error);
}
});
return loadModulesPromise;
}

View file

@ -23,27 +23,34 @@ export function loadNewJobCapabilities(
jobType: JobType
) {
return new Promise(async (resolve, reject) => {
const serviceToUse =
jobType === ANOMALY_DETECTOR ? newJobCapsService : newJobCapsServiceAnalytics;
if (indexPatternId !== undefined) {
// index pattern is being used
const indexPattern: IIndexPattern = await indexPatterns.get(indexPatternId);
await serviceToUse.initializeFromIndexPattern(indexPattern);
resolve(serviceToUse.newJobCaps);
} else if (savedSearchId !== undefined) {
// saved search is being used
// load the index pattern from the saved search
const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId);
if (indexPattern === null) {
// eslint-disable-next-line no-console
console.error('Cannot retrieve index pattern from saved search');
try {
const serviceToUse =
jobType === ANOMALY_DETECTOR ? newJobCapsService : newJobCapsServiceAnalytics;
if (indexPatternId !== undefined) {
// index pattern is being used
const indexPattern: IIndexPattern = await indexPatterns.get(indexPatternId);
await serviceToUse.initializeFromIndexPattern(indexPattern);
resolve(serviceToUse.newJobCaps);
} else if (savedSearchId !== undefined) {
// saved search is being used
// load the index pattern from the saved search
const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId);
if (indexPattern === null) {
// eslint-disable-next-line no-console
console.error('Cannot retrieve index pattern from saved search');
reject();
return;
}
await serviceToUse.initializeFromIndexPattern(indexPattern);
resolve(serviceToUse.newJobCaps);
} else {
reject();
return;
}
await serviceToUse.initializeFromIndexPattern(indexPattern);
resolve(serviceToUse.newJobCaps);
} else {
reject();
} catch (error) {
reject(error);
}
});
}

View file

@ -25,33 +25,34 @@ export async function resolveEmbeddableAnomalyChartsUserInput(
const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
return new Promise(async (resolve, reject) => {
const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds);
const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds);
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
const influencers = anomalyDetectorService.extractInfluencers(jobs);
influencers.push(VIEW_BY_JOB_LABEL);
const modalSession = overlays.openModal(
toMountPoint(
<AnomalyChartsInitializer
defaultTitle={title}
initialInput={input}
onCreate={({ panelTitle, maxSeriesToPlot }) => {
modalSession.close();
resolve({
jobIds,
title: panelTitle,
maxSeriesToPlot,
});
}}
onCancel={() => {
modalSession.close();
reject();
}}
/>
)
);
try {
const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds);
const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds);
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
const influencers = anomalyDetectorService.extractInfluencers(jobs);
influencers.push(VIEW_BY_JOB_LABEL);
const modalSession = overlays.openModal(
toMountPoint(
<AnomalyChartsInitializer
defaultTitle={title}
initialInput={input}
onCreate={({ panelTitle, maxSeriesToPlot }) => {
modalSession.close();
resolve({
jobIds,
title: panelTitle,
maxSeriesToPlot,
});
}}
onCancel={() => {
modalSession.close();
reject();
}}
/>
)
);
} catch (error) {
reject(error);
}
});
}

View file

@ -25,31 +25,36 @@ export async function resolveAnomalySwimlaneUserInput(
const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
return new Promise(async (resolve, reject) => {
const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds);
const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds);
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
const influencers = anomalyDetectorService.extractInfluencers(jobs);
influencers.push(VIEW_BY_JOB_LABEL);
const modalSession = overlays.openModal(
toMountPoint(
<AnomalySwimlaneInitializer
defaultTitle={title}
influencers={influencers}
initialInput={input}
onCreate={({ panelTitle, viewBy, swimlaneType }) => {
modalSession.close();
resolve({ jobIds, title: panelTitle, swimlaneType, viewBy });
}}
onCancel={() => {
modalSession.close();
reject();
}}
/>
)
);
try {
const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds);
const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds);
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
const influencers = anomalyDetectorService.extractInfluencers(jobs);
influencers.push(VIEW_BY_JOB_LABEL);
const modalSession = overlays.openModal(
toMountPoint(
<AnomalySwimlaneInitializer
defaultTitle={title}
influencers={influencers}
initialInput={input}
onCreate={({ panelTitle, viewBy, swimlaneType }) => {
modalSession.close();
resolve({
jobIds,
title: panelTitle,
swimlaneType,
viewBy,
});
}}
onCancel={() => {
modalSession.close();
reject();
}}
/>
)
);
} catch (error) {
reject(error);
}
});
}

View file

@ -38,56 +38,65 @@ export async function resolveJobSelection(
} = coreStart;
return new Promise(async (resolve, reject) => {
const maps = {
groupsMap: getInitialGroupsMap([]),
jobsMap: {},
};
try {
const maps = {
groupsMap: getInitialGroupsMap([]),
jobsMap: {},
};
const tzConfig = uiSettings.get('dateFormat:tz');
const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess();
const tzConfig = uiSettings.get('dateFormat:tz');
const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess();
const onFlyoutClose = () => {
flyoutSession.close();
reject();
};
const onSelectionConfirmed = async ({
jobIds,
groups,
}: {
jobIds: string[];
groups: Array<{ groupId: string; jobIds: string[] }>;
}) => {
await flyoutSession.close();
resolve({ jobIds, groups });
};
const flyoutSession = coreStart.overlays.openFlyout(
toMountPoint(
<KibanaContextProvider services={{ ...coreStart, mlServices: getMlGlobalServices(http) }}>
<JobSelectorFlyout
selectedIds={selectedJobIds}
withTimeRangeSelector={false}
dateFormatTz={dateFormatTz}
singleSelection={false}
timeseriesOnly={true}
onFlyoutClose={onFlyoutClose}
onSelectionConfirmed={onSelectionConfirmed}
maps={maps}
/>
</KibanaContextProvider>
),
{
'data-test-subj': 'mlFlyoutJobSelector',
ownFocus: true,
closeButtonAriaLabel: 'jobSelectorFlyout',
}
);
// Close the flyout when user navigates out of the dashboard plugin
currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => {
if (appId !== DashboardConstants.DASHBOARDS_ID) {
const onFlyoutClose = () => {
flyoutSession.close();
}
});
reject();
};
const onSelectionConfirmed = async ({
jobIds,
groups,
}: {
jobIds: string[];
groups: Array<{
groupId: string;
jobIds: string[];
}>;
}) => {
await flyoutSession.close();
resolve({
jobIds,
groups,
});
};
const flyoutSession = coreStart.overlays.openFlyout(
toMountPoint(
<KibanaContextProvider services={{ ...coreStart, mlServices: getMlGlobalServices(http) }}>
<JobSelectorFlyout
selectedIds={selectedJobIds}
withTimeRangeSelector={false}
dateFormatTz={dateFormatTz}
singleSelection={false}
timeseriesOnly={true}
onFlyoutClose={onFlyoutClose}
onSelectionConfirmed={onSelectionConfirmed}
maps={maps}
/>
</KibanaContextProvider>
),
{
'data-test-subj': 'mlFlyoutJobSelector',
ownFocus: true,
closeButtonAriaLabel: 'jobSelectorFlyout',
}
); // Close the flyout when user navigates out of the dashboard plugin
currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => {
if (appId !== DashboardConstants.DASHBOARDS_ID) {
flyoutSession.close();
}
});
} catch (error) {
reject(error);
}
});
}

View file

@ -98,27 +98,39 @@ async function getPaginatedThroughputData(pipelines, req, lsIndexPattern, throug
const metricSeriesData = Object.values(
await Promise.all(
pipelines.map((pipeline) => {
return new Promise(async (resolve) => {
const data = await getMetrics(
req,
lsIndexPattern,
[throughputMetric],
[
{
bool: {
should: [
{ term: { type: 'logstash_stats' } },
{ term: { 'metricset.name': 'stats' } },
],
return new Promise(async (resolve, reject) => {
try {
const data = await getMetrics(
req,
lsIndexPattern,
[throughputMetric],
[
{
bool: {
should: [
{
term: {
type: 'logstash_stats',
},
},
{
term: {
'metricset.name': 'stats',
},
},
],
},
},
],
{
pipeline,
},
],
{
pipeline,
},
2
);
resolve(reduceData(pipeline, data));
2
);
resolve(reduceData(pipeline, data));
} catch (error) {
reject(error);
}
});
})
)
@ -184,27 +196,38 @@ async function getPipelines(req, lsIndexPattern, pipelines, throughputMetric, no
async function getThroughputPipelines(req, lsIndexPattern, pipelines, throughputMetric) {
const metricsResponse = await Promise.all(
pipelines.map((pipeline) => {
return new Promise(async (resolve) => {
const data = await getMetrics(
req,
lsIndexPattern,
[throughputMetric],
[
{
bool: {
should: [
{ term: { type: 'logstash_stats' } },
{ term: { 'metricset.name': 'stats' } },
],
return new Promise(async (resolve, reject) => {
try {
const data = await getMetrics(
req,
lsIndexPattern,
[throughputMetric],
[
{
bool: {
should: [
{
term: {
type: 'logstash_stats',
},
},
{
term: {
'metricset.name': 'stats',
},
},
],
},
},
},
],
{
pipeline,
}
);
resolve(reduceData(pipeline, data));
],
{
pipeline,
}
);
resolve(reduceData(pipeline, data));
} catch (error) {
reject(error);
}
});
})
);

View file

@ -317,69 +317,73 @@ export class SessionIndex {
const sessionIndexTemplateName = `${this.options.kibanaIndexName}_security_session_index_template_${SESSION_INDEX_TEMPLATE_VERSION}`;
return (this.indexInitialization = new Promise<void>(async (resolve, reject) => {
// Check if required index template exists.
let indexTemplateExists = false;
try {
indexTemplateExists = (
await this.options.elasticsearchClient.indices.existsTemplate({
name: sessionIndexTemplateName,
})
).body;
} catch (err) {
this.options.logger.error(
`Failed to check if session index template exists: ${err.message}`
);
return reject(err);
}
// Create index template if it doesn't exist.
if (indexTemplateExists) {
this.options.logger.debug('Session index template already exists.');
} else {
// Check if required index template exists.
let indexTemplateExists = false;
try {
await this.options.elasticsearchClient.indices.putTemplate({
name: sessionIndexTemplateName,
body: getSessionIndexTemplate(this.indexName),
});
this.options.logger.debug('Successfully created session index template.');
indexTemplateExists = (
await this.options.elasticsearchClient.indices.existsTemplate({
name: sessionIndexTemplateName,
})
).body;
} catch (err) {
this.options.logger.error(`Failed to create session index template: ${err.message}`);
this.options.logger.error(
`Failed to check if session index template exists: ${err.message}`
);
return reject(err);
}
}
// Check if required index exists. We cannot be sure that automatic creation of indices is
// always enabled, so we create session index explicitly.
let indexExists = false;
try {
indexExists = (
await this.options.elasticsearchClient.indices.exists({ index: this.indexName })
).body;
} catch (err) {
this.options.logger.error(`Failed to check if session index exists: ${err.message}`);
return reject(err);
}
// Create index if it doesn't exist.
if (indexExists) {
this.options.logger.debug('Session index already exists.');
} else {
try {
await this.options.elasticsearchClient.indices.create({ index: this.indexName });
this.options.logger.debug('Successfully created session index.');
} catch (err) {
// There can be a race condition if index is created by another Kibana instance.
if (err?.body?.error?.type === 'resource_already_exists_exception') {
this.options.logger.debug('Session index already exists.');
} else {
this.options.logger.error(`Failed to create session index: ${err.message}`);
// Create index template if it doesn't exist.
if (indexTemplateExists) {
this.options.logger.debug('Session index template already exists.');
} else {
try {
await this.options.elasticsearchClient.indices.putTemplate({
name: sessionIndexTemplateName,
body: getSessionIndexTemplate(this.indexName),
});
this.options.logger.debug('Successfully created session index template.');
} catch (err) {
this.options.logger.error(`Failed to create session index template: ${err.message}`);
return reject(err);
}
}
}
// Notify any consumers that are awaiting on this promise and immediately reset it.
resolve();
// Check if required index exists. We cannot be sure that automatic creation of indices is
// always enabled, so we create session index explicitly.
let indexExists = false;
try {
indexExists = (
await this.options.elasticsearchClient.indices.exists({ index: this.indexName })
).body;
} catch (err) {
this.options.logger.error(`Failed to check if session index exists: ${err.message}`);
return reject(err);
}
// Create index if it doesn't exist.
if (indexExists) {
this.options.logger.debug('Session index already exists.');
} else {
try {
await this.options.elasticsearchClient.indices.create({ index: this.indexName });
this.options.logger.debug('Successfully created session index.');
} catch (err) {
// There can be a race condition if index is created by another Kibana instance.
if (err?.body?.error?.type === 'resource_already_exists_exception') {
this.options.logger.debug('Session index already exists.');
} else {
this.options.logger.error(`Failed to create session index: ${err.message}`);
return reject(err);
}
}
}
// Notify any consumers that are awaiting on this promise and immediately reset it.
resolve();
} catch (error) {
reject(error);
}
}).finally(() => {
this.indexInitialization = undefined;
}));

View file

@ -128,208 +128,225 @@ export const importRulesRoute = (
const batchParseObjects = chunkParseObjects.shift() ?? [];
const newImportRuleResponse = await Promise.all(
batchParseObjects.reduce<Array<Promise<ImportRuleResponse>>>((accum, parsedRule) => {
const importsWorkerPromise = new Promise<ImportRuleResponse>(async (resolve) => {
if (parsedRule instanceof Error) {
// If the JSON object had a validation or parse error then we return
// early with the error and an (unknown) for the ruleId
resolve(
createBulkErrorObject({
statusCode: 400,
message: parsedRule.message,
})
);
return null;
}
const {
anomaly_threshold: anomalyThreshold,
author,
building_block_type: buildingBlockType,
description,
enabled,
event_category_override: eventCategoryOverride,
false_positives: falsePositives,
from,
immutable,
query: queryOrUndefined,
language: languageOrUndefined,
license,
machine_learning_job_id: machineLearningJobId,
output_index: outputIndex,
saved_id: savedId,
meta,
filters: filtersRest,
rule_id: ruleId,
index,
interval,
max_signals: maxSignals,
risk_score: riskScore,
risk_score_mapping: riskScoreMapping,
rule_name_override: ruleNameOverride,
name,
severity,
severity_mapping: severityMapping,
tags,
threat,
threat_filters: threatFilters,
threat_index: threatIndex,
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
threat_indicator_path: threatIndicatorPath,
concurrent_searches: concurrentSearches,
items_per_search: itemsPerSearch,
threshold,
timestamp_override: timestampOverride,
to,
type,
references,
note,
timeline_id: timelineId,
timeline_title: timelineTitle,
throttle,
version,
exceptions_list: exceptionsList,
} = parsedRule;
const importsWorkerPromise = new Promise<ImportRuleResponse>(
async (resolve, reject) => {
try {
if (parsedRule instanceof Error) {
// If the JSON object had a validation or parse error then we return
// early with the error and an (unknown) for the ruleId
resolve(
createBulkErrorObject({
statusCode: 400,
message: parsedRule.message,
})
);
return null;
}
try {
const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined;
const language =
!isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined;
// TODO: Fix these either with an is conversion or by better typing them within io-ts
const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[];
throwHttpError(await mlAuthz.validateRuleType(type));
const rule = await readRules({ rulesClient, ruleId, id: undefined });
if (rule == null) {
await createRules({
rulesClient,
anomalyThreshold,
const {
anomaly_threshold: anomalyThreshold,
author,
buildingBlockType,
building_block_type: buildingBlockType,
description,
enabled,
eventCategoryOverride,
falsePositives,
event_category_override: eventCategoryOverride,
false_positives: falsePositives,
from,
immutable,
query,
language,
query: queryOrUndefined,
language: languageOrUndefined,
license,
machineLearningJobId,
outputIndex: signalsIndex,
savedId,
timelineId,
timelineTitle,
machine_learning_job_id: machineLearningJobId,
output_index: outputIndex,
saved_id: savedId,
meta,
filters,
ruleId,
filters: filtersRest,
rule_id: ruleId,
index,
interval,
maxSignals,
name,
riskScore,
riskScoreMapping,
ruleNameOverride,
severity,
severityMapping,
tags,
throttle,
to,
type,
threat,
threshold,
threatFilters,
threatIndex,
threatIndicatorPath,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
timestampOverride,
references,
note,
version,
exceptionsList,
actions: [], // Actions are not imported nor exported at this time
});
resolve({ rule_id: ruleId, status_code: 200 });
} else if (rule != null && request.query.overwrite) {
await patchRules({
rulesClient,
author,
buildingBlockType,
spaceId: context.securitySolution.getSpaceId(),
ruleStatusClient,
description,
enabled,
eventCategoryOverride,
falsePositives,
from,
query,
language,
license,
outputIndex,
savedId,
timelineId,
timelineTitle,
meta,
filters,
rule,
index,
interval,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
max_signals: maxSignals,
risk_score: riskScore,
risk_score_mapping: riskScoreMapping,
rule_name_override: ruleNameOverride,
name,
severity,
severityMapping,
severity_mapping: severityMapping,
tags,
timestampOverride,
throttle,
threat,
threat_filters: threatFilters,
threat_index: threatIndex,
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
threat_indicator_path: threatIndicatorPath,
concurrent_searches: concurrentSearches,
items_per_search: itemsPerSearch,
threshold,
timestamp_override: timestampOverride,
to,
type,
threat,
threshold,
threatFilters,
threatIndex,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
references,
note,
timeline_id: timelineId,
timeline_title: timelineTitle,
throttle,
version,
exceptionsList,
anomalyThreshold,
machineLearningJobId,
actions: undefined,
});
resolve({ rule_id: ruleId, status_code: 200 });
} else if (rule != null) {
resolve(
createBulkErrorObject({
exceptions_list: exceptionsList,
} = parsedRule;
try {
const query =
!isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined;
const language =
!isMlRule(type) && languageOrUndefined == null
? 'kuery'
: languageOrUndefined; // TODO: Fix these either with an is conversion or by better typing them within io-ts
const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[];
throwHttpError(await mlAuthz.validateRuleType(type));
const rule = await readRules({
rulesClient,
ruleId,
statusCode: 409,
message: `rule_id: "${ruleId}" already exists`,
})
);
id: undefined,
});
if (rule == null) {
await createRules({
rulesClient,
anomalyThreshold,
author,
buildingBlockType,
description,
enabled,
eventCategoryOverride,
falsePositives,
from,
immutable,
query,
language,
license,
machineLearningJobId,
outputIndex: signalsIndex,
savedId,
timelineId,
timelineTitle,
meta,
filters,
ruleId,
index,
interval,
maxSignals,
name,
riskScore,
riskScoreMapping,
ruleNameOverride,
severity,
severityMapping,
tags,
throttle,
to,
type,
threat,
threshold,
threatFilters,
threatIndex,
threatIndicatorPath,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
timestampOverride,
references,
note,
version,
exceptionsList,
actions: [], // Actions are not imported nor exported at this time
});
resolve({
rule_id: ruleId,
status_code: 200,
});
} else if (rule != null && request.query.overwrite) {
await patchRules({
rulesClient,
author,
buildingBlockType,
spaceId: context.securitySolution.getSpaceId(),
ruleStatusClient,
description,
enabled,
eventCategoryOverride,
falsePositives,
from,
query,
language,
license,
outputIndex,
savedId,
timelineId,
timelineTitle,
meta,
filters,
rule,
index,
interval,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
severity,
severityMapping,
tags,
timestampOverride,
throttle,
to,
type,
threat,
threshold,
threatFilters,
threatIndex,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
references,
note,
version,
exceptionsList,
anomalyThreshold,
machineLearningJobId,
actions: undefined,
});
resolve({
rule_id: ruleId,
status_code: 200,
});
} else if (rule != null) {
resolve(
createBulkErrorObject({
ruleId,
statusCode: 409,
message: `rule_id: "${ruleId}" already exists`,
})
);
}
} catch (err) {
resolve(
createBulkErrorObject({
ruleId,
statusCode: err.statusCode ?? 400,
message: err.message,
})
);
}
} catch (error) {
reject(error);
}
} catch (err) {
resolve(
createBulkErrorObject({
ruleId,
statusCode: err.statusCode ?? 400,
message: err.message,
})
);
}
});
);
return [...accum, importsWorkerPromise];
}, [])
);

View file

@ -106,133 +106,132 @@ export const importTimelines = async (
batchParseObjects.reduce<Array<Promise<ImportTimelineResponse>>>((accum, parsedTimeline) => {
const importsWorkerPromise = new Promise<ImportTimelineResponse>(
async (resolve, reject) => {
if (parsedTimeline instanceof Error) {
// If the JSON object had a validation or parse error then we return
// early with the error and an (unknown) for the ruleId
resolve(
createBulkErrorObject({
statusCode: 400,
message: parsedTimeline.message,
})
);
return null;
}
const {
savedObjectId,
pinnedEventIds,
globalNotes,
eventNotes,
status,
templateTimelineId,
templateTimelineVersion,
title,
timelineType,
version,
} = parsedTimeline;
const parsedTimelineObject = omit(timelineSavedObjectOmittedFields, parsedTimeline);
let newTimeline = null;
try {
const compareTimelinesStatus = new CompareTimelinesStatus({
status,
timelineType,
title,
timelineInput: {
id: savedObjectId,
version,
},
templateTimelineInput: {
id: templateTimelineId,
version: templateTimelineVersion,
},
frameworkRequest,
});
await compareTimelinesStatus.init();
const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline;
if (compareTimelinesStatus.isCreatableViaImport) {
// create timeline / timeline template
newTimeline = await createTimelines({
frameworkRequest,
timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline),
pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds,
notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes],
isImmutable,
overrideNotesOwner: false,
});
resolve({
timeline_id: newTimeline.timeline.savedObjectId,
status_code: 200,
action: TimelineStatusActions.createViaImport,
});
}
if (!compareTimelinesStatus.isHandlingTemplateTimeline) {
const errorMessage = compareTimelinesStatus.checkIsFailureCases(
TimelineStatusActions.createViaImport
);
const message = errorMessage?.body ?? DEFAULT_ERROR;
if (parsedTimeline instanceof Error) {
// If the JSON object had a validation or parse error then we return
// early with the error and an (unknown) for the ruleId
resolve(
createBulkErrorObject({
id: savedObjectId ?? 'unknown',
statusCode: 409,
message,
statusCode: 400,
message: parsedTimeline.message,
})
);
} else {
if (compareTimelinesStatus.isUpdatableViaImport) {
// update timeline template
return null;
}
const {
savedObjectId,
pinnedEventIds,
globalNotes,
eventNotes,
status,
templateTimelineId,
templateTimelineVersion,
title,
timelineType,
version,
} = parsedTimeline;
const parsedTimelineObject = omit(timelineSavedObjectOmittedFields, parsedTimeline);
let newTimeline = null;
try {
const compareTimelinesStatus = new CompareTimelinesStatus({
status,
timelineType,
title,
timelineInput: {
id: savedObjectId,
version,
},
templateTimelineInput: {
id: templateTimelineId,
version: templateTimelineVersion,
},
frameworkRequest,
});
await compareTimelinesStatus.init();
const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline;
if (compareTimelinesStatus.isCreatableViaImport) {
// create timeline / timeline template
newTimeline = await createTimelines({
frameworkRequest,
timeline: parsedTimelineObject,
timelineSavedObjectId: compareTimelinesStatus.timelineId,
timelineVersion: compareTimelinesStatus.timelineVersion,
notes: globalNotes,
existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds,
timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline),
pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds,
notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes],
isImmutable,
overrideNotesOwner: false,
});
resolve({
timeline_id: newTimeline.timeline.savedObjectId,
status_code: 200,
action: TimelineStatusActions.updateViaImport,
action: TimelineStatusActions.createViaImport,
});
} else {
}
if (!compareTimelinesStatus.isHandlingTemplateTimeline) {
const errorMessage = compareTimelinesStatus.checkIsFailureCases(
TimelineStatusActions.updateViaImport
TimelineStatusActions.createViaImport
);
const message = errorMessage?.body ?? DEFAULT_ERROR;
resolve(
createBulkErrorObject({
id:
savedObjectId ??
(templateTimelineId
? `(template_timeline_id) ${templateTimelineId}`
: 'unknown'),
id: savedObjectId ?? 'unknown',
statusCode: 409,
message,
})
);
} else {
if (compareTimelinesStatus.isUpdatableViaImport) {
// update timeline template
newTimeline = await createTimelines({
frameworkRequest,
timeline: parsedTimelineObject,
timelineSavedObjectId: compareTimelinesStatus.timelineId,
timelineVersion: compareTimelinesStatus.timelineVersion,
notes: globalNotes,
existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds,
isImmutable,
overrideNotesOwner: false,
});
resolve({
timeline_id: newTimeline.timeline.savedObjectId,
status_code: 200,
action: TimelineStatusActions.updateViaImport,
});
} else {
const errorMessage = compareTimelinesStatus.checkIsFailureCases(
TimelineStatusActions.updateViaImport
);
const message = errorMessage?.body ?? DEFAULT_ERROR;
resolve(
createBulkErrorObject({
id:
savedObjectId ??
(templateTimelineId
? `(template_timeline_id) ${templateTimelineId}`
: 'unknown'),
statusCode: 409,
message,
})
);
}
}
} catch (err) {
resolve(
createBulkErrorObject({
id:
savedObjectId ??
(templateTimelineId
? `(template_timeline_id) ${templateTimelineId}`
: 'unknown'),
statusCode: 400,
message: err.message,
})
);
}
} catch (err) {
resolve(
createBulkErrorObject({
id:
savedObjectId ??
(templateTimelineId
? `(template_timeline_id) ${templateTimelineId}`
: 'unknown'),
statusCode: 400,
message: err.message,
})
);
} catch (error) {
reject(error);
}
}
);

View file

@ -47,62 +47,62 @@ describe('Configuration Statistics Aggregator', () => {
};
return new Promise<void>(async (resolve, reject) => {
createConfigurationAggregator(configuration, managedConfig)
.pipe(take(3), bufferCount(3))
.subscribe(([initial, updatedWorkers, updatedInterval]) => {
expect(initial.value).toEqual({
max_workers: 10,
poll_interval: 6000000,
max_poll_inactivity_cycles: 10,
request_capacity: 1000,
monitored_aggregated_stats_refresh_rate: 5000,
monitored_stats_running_average_window: 50,
monitored_task_execution_thresholds: {
default: {
error_threshold: 90,
warn_threshold: 80,
try {
createConfigurationAggregator(configuration, managedConfig)
.pipe(take(3), bufferCount(3))
.subscribe(([initial, updatedWorkers, updatedInterval]) => {
expect(initial.value).toEqual({
max_workers: 10,
poll_interval: 6000000,
max_poll_inactivity_cycles: 10,
request_capacity: 1000,
monitored_aggregated_stats_refresh_rate: 5000,
monitored_stats_running_average_window: 50,
monitored_task_execution_thresholds: {
default: {
error_threshold: 90,
warn_threshold: 80,
},
custom: {},
},
custom: {},
},
});
expect(updatedWorkers.value).toEqual({
max_workers: 8,
poll_interval: 6000000,
max_poll_inactivity_cycles: 10,
request_capacity: 1000,
monitored_aggregated_stats_refresh_rate: 5000,
monitored_stats_running_average_window: 50,
monitored_task_execution_thresholds: {
default: {
error_threshold: 90,
warn_threshold: 80,
});
expect(updatedWorkers.value).toEqual({
max_workers: 8,
poll_interval: 6000000,
max_poll_inactivity_cycles: 10,
request_capacity: 1000,
monitored_aggregated_stats_refresh_rate: 5000,
monitored_stats_running_average_window: 50,
monitored_task_execution_thresholds: {
default: {
error_threshold: 90,
warn_threshold: 80,
},
custom: {},
},
custom: {},
},
});
expect(updatedInterval.value).toEqual({
max_workers: 8,
poll_interval: 3000,
max_poll_inactivity_cycles: 10,
request_capacity: 1000,
monitored_aggregated_stats_refresh_rate: 5000,
monitored_stats_running_average_window: 50,
monitored_task_execution_thresholds: {
default: {
error_threshold: 90,
warn_threshold: 80,
});
expect(updatedInterval.value).toEqual({
max_workers: 8,
poll_interval: 3000,
max_poll_inactivity_cycles: 10,
request_capacity: 1000,
monitored_aggregated_stats_refresh_rate: 5000,
monitored_stats_running_average_window: 50,
monitored_task_execution_thresholds: {
default: {
error_threshold: 90,
warn_threshold: 80,
},
custom: {},
},
custom: {},
},
});
resolve();
}, reject);
managedConfig.maxWorkersConfiguration$.next(8);
managedConfig.pollIntervalConfiguration$.next(3000);
});
resolve();
}, reject);
managedConfig.maxWorkersConfiguration$.next(8);
managedConfig.pollIntervalConfiguration$.next(3000);
} catch (error) {
reject(error);
}
});
});
});

View file

@ -328,27 +328,44 @@ describe('Workload Statistics Aggregator', () => {
loggingSystemMock.create().get()
);
return new Promise<void>(async (resolve) => {
workloadAggregator.pipe(first()).subscribe((result) => {
expect(result.key).toEqual('workload');
expect(result.value).toMatchObject({
count: 4,
task_types: {
actions_telemetry: { count: 2, status: { idle: 2 } },
alerting_telemetry: { count: 1, status: { idle: 1 } },
session_cleanup: { count: 1, status: { idle: 1 } },
},
return new Promise<void>(async (resolve, reject) => {
try {
workloadAggregator.pipe(first()).subscribe((result) => {
expect(result.key).toEqual('workload');
expect(result.value).toMatchObject({
count: 4,
task_types: {
actions_telemetry: {
count: 2,
status: {
idle: 2,
},
},
alerting_telemetry: {
count: 1,
status: {
idle: 1,
},
},
session_cleanup: {
count: 1,
status: {
idle: 1,
},
},
},
});
resolve();
});
resolve();
});
availability$.next(false);
await sleep(10);
expect(taskStore.aggregate).not.toHaveBeenCalled();
await sleep(10);
expect(taskStore.aggregate).not.toHaveBeenCalled();
availability$.next(true);
availability$.next(false);
await sleep(10);
expect(taskStore.aggregate).not.toHaveBeenCalled();
await sleep(10);
expect(taskStore.aggregate).not.toHaveBeenCalled();
availability$.next(true);
} catch (error) {
reject(error);
}
});
});

View file

@ -113,11 +113,18 @@ export class TaskScheduling {
*/
public async runNow(taskId: string): Promise<RunNowResult> {
return new Promise(async (resolve, reject) => {
this.awaitTaskRunResult(taskId)
// don't expose state on runNow
.then(({ id }) => resolve({ id }))
.catch(reject);
this.taskPollingLifecycle.attemptToRun(taskId);
try {
this.awaitTaskRunResult(taskId) // don't expose state on runNow
.then(({ id }) =>
resolve({
id,
})
)
.catch(reject);
this.taskPollingLifecycle.attemptToRun(taskId);
} catch (error) {
reject(error);
}
});
}
@ -137,39 +144,42 @@ export class TaskScheduling {
taskInstance: task,
});
return new Promise(async (resolve, reject) => {
// The actual promise returned from this function is resolved after the awaitTaskRunResult promise resolves.
// However, we do not wait to await this promise, as we want later execution to happen in parallel.
// The awaitTaskRunResult promise is resolved once the ephemeral task is successfully executed (technically, when a TaskEventType.TASK_RUN is emitted with the same id).
// However, the ephemeral task won't even get into the queue until the subsequent this.ephemeralTaskLifecycle.attemptToRun is called (which puts it in the queue).
// The reason for all this confusion? Timing.
// In the this.ephemeralTaskLifecycle.attemptToRun, it's possible that the ephemeral task is put into the queue and processed before this function call returns anything.
// If that happens, putting the awaitTaskRunResult after would just hang because the task already completed. We need to listen for the completion before we add it to the queue to avoid this possibility.
const { cancel, resolveOnCancel } = cancellablePromise();
this.awaitTaskRunResult(id, resolveOnCancel)
.then((arg: RunNowResult) => {
resolve(arg);
})
.catch((err: Error) => {
reject(err);
try {
// The actual promise returned from this function is resolved after the awaitTaskRunResult promise resolves.
// However, we do not wait to await this promise, as we want later execution to happen in parallel.
// The awaitTaskRunResult promise is resolved once the ephemeral task is successfully executed (technically, when a TaskEventType.TASK_RUN is emitted with the same id).
// However, the ephemeral task won't even get into the queue until the subsequent this.ephemeralTaskLifecycle.attemptToRun is called (which puts it in the queue).
// The reason for all this confusion? Timing.
// In the this.ephemeralTaskLifecycle.attemptToRun, it's possible that the ephemeral task is put into the queue and processed before this function call returns anything.
// If that happens, putting the awaitTaskRunResult after would just hang because the task already completed. We need to listen for the completion before we add it to the queue to avoid this possibility.
const { cancel, resolveOnCancel } = cancellablePromise();
this.awaitTaskRunResult(id, resolveOnCancel)
.then((arg: RunNowResult) => {
resolve(arg);
})
.catch((err: Error) => {
reject(err);
});
const attemptToRunResult = this.ephemeralTaskLifecycle.attemptToRun({
id,
scheduledAt: new Date(),
runAt: new Date(),
status: TaskStatus.Idle,
ownerId: this.taskManagerId,
...modifiedTask,
});
const attemptToRunResult = this.ephemeralTaskLifecycle.attemptToRun({
id,
scheduledAt: new Date(),
runAt: new Date(),
status: TaskStatus.Idle,
ownerId: this.taskManagerId,
...modifiedTask,
});
if (isErr(attemptToRunResult)) {
cancel();
reject(
new EphemeralTaskRejectedDueToCapacityError(
`Ephemeral Task of type ${task.taskType} was rejected`,
task
)
);
if (isErr(attemptToRunResult)) {
cancel();
reject(
new EphemeralTaskRejectedDueToCapacityError(
`Ephemeral Task of type ${task.taskType} was rejected`,
task
)
);
}
} catch (error) {
reject(error);
}
});
}

View file

@ -105,14 +105,20 @@ const search = async (engineName: string): Promise<ISearchResponse> => {
// Since the App Search API does not issue document receipts, the only way to tell whether or not documents
// are fully indexed is to poll the search endpoint.
export const waitForIndexedDocs = (engineName: string) => {
return new Promise<void>(async function (resolve) {
let isReady = false;
while (!isReady) {
const response = await search(engineName);
if (response.results && response.results.length > 0) {
isReady = true;
resolve();
return new Promise<void>(async function (resolve, reject) {
try {
let isReady = false;
while (!isReady) {
const response = await search(engineName);
if (response.results && response.results.length > 0) {
isReady = true;
resolve();
}
}
} catch (error) {
reject(error);
}
});
};

View file

@ -116,21 +116,29 @@ export const waitFor = async (
timeoutWait: number = 10
) => {
await new Promise<void>(async (resolve, reject) => {
let found = false;
let numberOfTries = 0;
while (!found && numberOfTries < Math.floor(maxTimeout / timeoutWait)) {
const itPasses = await functionToTest();
if (itPasses) {
found = true;
} else {
numberOfTries++;
try {
let found = false;
let numberOfTries = 0;
while (!found && numberOfTries < Math.floor(maxTimeout / timeoutWait)) {
const itPasses = await functionToTest();
if (itPasses) {
found = true;
} else {
numberOfTries++;
}
await new Promise((resolveTimeout) => setTimeout(resolveTimeout, timeoutWait));
}
await new Promise((resolveTimeout) => setTimeout(resolveTimeout, timeoutWait));
}
if (found) {
resolve();
} else {
reject(new Error(`timed out waiting for function ${functionName} condition to be true`));
if (found) {
resolve();
} else {
reject(new Error(`timed out waiting for function ${functionName} condition to be true`));
}
} catch (error) {
reject(error);
}
});
};

View file

@ -6457,6 +6457,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.1.tgz#b3d2eb91dafd0fd8b3fce7c61512ac66bd0364aa"
integrity sha512-SkhzHdI/AllAgQSxXM89XwS1Tkic7csPdndUuTKabEwRcEfR8uQ/iPA3Dgio1rqsV3jtqZhY0QQni8rLswJM2w==
"@typescript-eslint/types@4.28.3":
version "4.28.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.3.tgz#8fffd436a3bada422c2c1da56060a0566a9506c7"
integrity sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA==
"@typescript-eslint/types@4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.3.0.tgz#1f0b2d5e140543e2614f06d48fb3ae95193c6ddf"
@ -6490,6 +6495,19 @@
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/typescript-estree@^4.14.1":
version "4.28.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz#253d7088100b2a38aefe3c8dd7bd1f8232ec46fb"
integrity sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w==
dependencies:
"@typescript-eslint/types" "4.28.3"
"@typescript-eslint/visitor-keys" "4.28.3"
debug "^4.3.1"
globby "^11.0.3"
is-glob "^4.0.1"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@4.14.1":
version "4.14.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.1.tgz#e93c2ff27f47ee477a929b970ca89d60a117da91"
@ -6498,6 +6516,14 @@
"@typescript-eslint/types" "4.14.1"
eslint-visitor-keys "^2.0.0"
"@typescript-eslint/visitor-keys@4.28.3":
version "4.28.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz#26ac91e84b23529968361045829da80a4e5251c4"
integrity sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg==
dependencies:
"@typescript-eslint/types" "4.28.3"
eslint-visitor-keys "^2.0.0"
"@typescript-eslint/visitor-keys@4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz#0e5ab0a09552903edeae205982e8521e17635ae0"
@ -13120,6 +13146,11 @@ eslint-scope@^5.0.0:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-traverse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-traverse/-/eslint-traverse-1.0.0.tgz#108d360a171a6e6334e1af0cee905a93bd0dcc53"
integrity sha512-bSp37rQs93LF8rZ409EI369DGCI4tELbFVmFNxI6QbuveS7VRxYVyUhwDafKN/enMyUh88HQQ7ZoGUHtPuGdcw==
eslint-utils@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
@ -27535,6 +27566,13 @@ tsutils@^3.17.1:
dependencies:
tslib "^1.8.1"
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
dependencies:
tslib "^1.8.1"
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"