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

[backend] Prevent prepareElementForIndexing to block event loop #9744

Merged
merged 3 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
56 changes: 39 additions & 17 deletions opencti-platform/opencti-graphql/src/database/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -3864,39 +3864,61 @@ const createDeleteOperationElement = async (context, user, mainElement, deletedE
};

// TODO: get rid of this function and let elastic fail queries, so we can fix all of them by using the right type of data
export const prepareElementForIndexing = (element) => {
export const prepareElementForIndexing = async (element) => {
const thing = {};
Object.keys(element).forEach((key) => {
const keyItems = Object.keys(element);
let startProcessingTime = new Date().getTime();
for (let index = 0; index < keyItems.length; index += 1) {
const key = keyItems[index];
const value = element[key];
if (Array.isArray(value)) { // Array of Date, objects, string or number
const filteredArray = value.filter((i) => i);
thing[key] = filteredArray.length > 0 ? filteredArray.map((f) => {
if (isDateAttribute(key)) { // Date is an object but natively supported
return f;
}
if (R.is(String, f)) { // For string, trim by default
return f.trim();
const preparedArray = [];
let innerProcessingTime = new Date().getTime();
for (let valueIndex = 0; valueIndex < value.length; valueIndex += 1) {
const valueElement = value[valueIndex];
if (valueElement) {
if (isDateAttribute(key)) { // Date is an object but natively supported
preparedArray.push(valueElement);
} else if (R.is(String, valueElement)) { // For string, trim by default
preparedArray.push(valueElement.trim());
} else if (R.is(Object, valueElement) && Object.keys(value).length > 0) { // For complex object, prepare inner elements
const complexPrepared = await prepareElementForIndexing(valueElement);
preparedArray.push(complexPrepared);
} else {
// For all other types, no transform (list of boolean is not supported)
preparedArray.push(valueElement);
}
}
if (R.is(Object, f) && Object.keys(value).length > 0) { // For complex object, prepare inner elements
return prepareElementForIndexing(f);
// Prevent event loop locking more than MAX_EVENT_LOOP_PROCESSING_TIME
if (new Date().getTime() - innerProcessingTime > MAX_EVENT_LOOP_PROCESSING_TIME) {
innerProcessingTime = new Date().getTime();
await new Promise((resolve) => {
setImmediate(resolve);
});
}
// For all other types, no transform (list of boolean is not supported)
return f;
}) : [];
}
thing[key] = preparedArray;
} else if (isDateAttribute(key)) { // Date is an object but natively supported
thing[key] = value;
} else if (isBooleanAttribute(key)) { // Patch field is string generic so need to be cast to boolean
thing[key] = typeof value === 'boolean' ? value : value?.toLowerCase() === 'true';
} else if (isNumericAttribute(key)) {
thing[key] = isNotEmptyField(value) ? Number(value) : undefined;
} else if (R.is(Object, value) && Object.keys(value).length > 0) { // For complex object, prepare inner elements
thing[key] = prepareElementForIndexing(value);
thing[key] = await prepareElementForIndexing(value);
} else if (R.is(String, value)) { // For string, trim by default
thing[key] = value.trim();
} else { // For all other types (numeric, ...), no transform
thing[key] = value;
}
});
// Prevent event loop locking more than MAX_EVENT_LOOP_PROCESSING_TIME
if (new Date().getTime() - startProcessingTime > MAX_EVENT_LOOP_PROCESSING_TIME) {
startProcessingTime = new Date().getTime();
await new Promise((resolve) => {
setImmediate(resolve);
});
}
}
return thing;
};
const prepareRelation = (thing) => {
Expand Down Expand Up @@ -4228,7 +4250,7 @@ const getInstanceToUpdate = async (context, user, instance) => {
};
export const elUpdateElement = async (context, user, instance) => {
const instanceToUse = await getInstanceToUpdate(context, user, instance);
const esData = prepareElementForIndexing(instanceToUse);
const esData = await prepareElementForIndexing(instanceToUse);
validateDataBeforeIndexing(esData);
const dataToReplace = R.pipe(R.dissoc('representative'), R.dissoc('_id'))(esData);
const replacePromise = elReplace(instanceToUse._index, instanceToUse._id ?? instanceToUse.internal_id, { doc: dataToReplace });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const up = async (next) => {
settingsFromEl.platform_entities_files_ref = null;
settingsFromEl.platform_hidden_types = null;

const esData = prepareElementForIndexing(settingsFromEl);
const esData = await prepareElementForIndexing(settingsFromEl);
await elReplace(settingsFromEl._index, settingsFromEl.internal_id, { doc: esData });

next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@ import { describe, expect, it } from 'vitest';
import { prepareElementForIndexing } from '../../../src/database/engine';

describe('prepareElementForIndexing testing', () => {
it('should base trim applied', () => {
const element = prepareElementForIndexing({ name: ' test' });
it('should base trim applied', async () => {
const element = await prepareElementForIndexing({ name: ' test' });
expect(element.name).toBe('test');
});
it('should inner trim applied', () => {
const element = prepareElementForIndexing({ num: 10, data: { test: ' spacing ' } });
it('should inner trim applied', async () => {
const element = await prepareElementForIndexing({ num: 10, data: { test: ' spacing ' } });
expect(element.num).toBe(10);
expect(element.data.test).toBe('spacing');
});
it('should array trim applied', () => {
const element = prepareElementForIndexing({ test: [20, ' trim01 ', ' trim 02 '] });
it('should array trim applied', async () => {
const element = await prepareElementForIndexing({ test: [20, ' trim01 ', ' trim 02 '] });
expect(element.test).toEqual([20, 'trim01', 'trim 02']);
});
it('should inner array trim applied', () => {
const element = prepareElementForIndexing({ test: { values: [20, ' trim01 ', ' trim 02 '] } });
it('should inner array trim applied', async () => {
const element = await prepareElementForIndexing({ test: { values: [20, ' trim01 ', ' trim 02 '] } });
expect(element.test.values).toEqual([20, 'trim01', 'trim 02']);
});
it('should do nothing with date value', () => {
it('should do nothing with date value', async () => {
const now = new Date();
const element = prepareElementForIndexing({ date: now });
const element = await prepareElementForIndexing({ date: now });
expect(element.date).toEqual(now);
});
});