feat: handle deletion of many indices in chunks

Signed-off-by: Thomas Cardonne <thomas.cardonne@adevinta.com>
This commit is contained in:
Thomas Cardonne
2024-05-23 14:10:04 +02:00
parent 3a5c6e6d8b
commit 5eba6fcd79
2 changed files with 79 additions and 9 deletions
@@ -114,7 +114,7 @@ describe('ElasticSearchSearchEngineIndexer', () => {
mock.add(
{
method: 'DELETE',
path: '/some-type-index__123tobedeleted%2Csome-type-index__456tobedeleted',
path: '/*',
},
deleteSpy,
);
@@ -161,7 +161,11 @@ describe('ElasticSearchSearchEngineIndexer', () => {
});
// Old index should be cleaned up.
expect(deleteSpy).toHaveBeenCalled();
expect(deleteSpy).toHaveBeenCalledWith(
expect.objectContaining({
path: '/some-type-index__123tobedeleted%2Csome-type-index__456tobedeleted',
}),
);
});
it('handles when no documents are received', async () => {
@@ -185,7 +189,11 @@ describe('ElasticSearchSearchEngineIndexer', () => {
expect(aliasesSpy).not.toHaveBeenCalled();
// Old index should not be cleaned up.
expect(deleteSpy).not.toHaveBeenCalled();
expect(deleteSpy).toHaveBeenCalledWith(
expect.objectContaining({
path: expect.not.stringContaining('tobedeleted'),
}),
);
});
it('handles bulk and batching during indexing', async () => {
@@ -240,6 +248,47 @@ describe('ElasticSearchSearchEngineIndexer', () => {
expect(deleteSpy).not.toHaveBeenCalled();
});
it('split index deletion in chunks', async () => {
// Generate 200 existing indices
const currentIndices = Array.from(
new Array(200),
(_, i) => `some-type-index__old${i}`,
);
const indicesResponse = currentIndices.reduce((acc, curr) => {
return {
...acc,
[curr]: { mappings: {}, aliases: {} },
};
}, {});
getSpy = jest.fn().mockReturnValue(indicesResponse);
mock.clear({
method: 'GET',
path: '/some-type-index__*',
});
mock.add(
{
method: 'GET',
path: '/some-type-index__*',
},
getSpy,
);
await TestPipeline.fromIndexer(indexer)
.withDocuments([
{
title: 'testTerm',
text: 'testText',
location: 'test/location',
},
])
.execute();
// Delete endpoint should have been called for 4 chunks of 50 (200 indices)
expect(deleteSpy).toHaveBeenCalledTimes(4);
});
it('handles bulk client rejection', async () => {
// Given an ES client wrapper that rejects an error
const expectedError = new Error('HTTP Timeout');
@@ -181,12 +181,33 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
this.logger.info('Removing stale search indices', {
removableIndices: this.removableIndices,
});
try {
await this.elasticSearchClientWrapper.deleteIndex({
index: this.removableIndices,
});
} catch (e) {
this.logger.warn(`Failed to remove stale search indices: ${e}`);
// Split the array into chunks of up to 50 indices to handle the case
// where we need to delete a lot of stalled indices
const chunks = this.removableIndices.reduce(
(resultArray, item, index) => {
const chunkIndex = Math.floor(index / 50);
if (!resultArray[chunkIndex]) {
resultArray[chunkIndex] = []; // start a new chunk
}
resultArray[chunkIndex].push(item);
return resultArray;
},
[] as string[][],
);
// Call deleteIndex for each chunk
for (const chunk of chunks) {
try {
await this.elasticSearchClientWrapper.deleteIndex({
index: chunk,
});
} catch (e) {
this.logger.warn(`Failed to remove stale search indices: ${e}`);
}
}
}
}