-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[SO Migration] fix reindex race on multi-instance mode #104516
Changes from 6 commits
a4ca9dc
641e63e
390adf8
1e26c56
f9fa0fe
5cbb075
c1bc869
7f3f35c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,9 @@ import { | |
catchRetryableEsClientErrors, | ||
RetryableEsClientError, | ||
} from './catch_retryable_es_client_errors'; | ||
import { isWriteBlockException } from './es_errors'; | ||
import { WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE } from './constants'; | ||
import type { TargetIndexHadWriteBlock } from './index'; | ||
|
||
/** @internal */ | ||
export interface BulkOverwriteTransformedDocumentsParams { | ||
|
@@ -24,6 +26,7 @@ export interface BulkOverwriteTransformedDocumentsParams { | |
transformedDocs: SavedObjectsRawDoc[]; | ||
refresh?: estypes.Refresh; | ||
} | ||
|
||
/** | ||
* Write the up-to-date transformed documents to the index, overwriting any | ||
* documents that are still on their outdated version. | ||
|
@@ -34,7 +37,7 @@ export const bulkOverwriteTransformedDocuments = ({ | |
transformedDocs, | ||
refresh = false, | ||
}: BulkOverwriteTransformedDocumentsParams): TaskEither.TaskEither< | ||
RetryableEsClientError, | ||
RetryableEsClientError | TargetIndexHadWriteBlock, | ||
'bulk_index_succeeded' | ||
> => () => { | ||
return client | ||
|
@@ -71,12 +74,19 @@ export const bulkOverwriteTransformedDocuments = ({ | |
.then((res) => { | ||
// Filter out version_conflict_engine_exception since these just mean | ||
// that another instance already updated these documents | ||
const errors = (res.body.items ?? []).filter( | ||
(item) => item.index?.error?.type !== 'version_conflict_engine_exception' | ||
); | ||
const errors = (res.body.items ?? []) | ||
.filter((item) => item.index?.error) | ||
.map((item) => item.index!.error!) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wow! so many .map((item) => item.index?.error)
.filter(Boolean)
.filter(({ type }) => type !== 'version_conflict_engine_exception'); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, the problem is that TS is stupid with map/filter. .map((item) => item.index?.error)
.filter(Boolean) Is not sufficient to have the |
||
.filter(({ type }) => type !== 'version_conflict_engine_exception'); | ||
Comment on lines
-74
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not even sure how this was working previously with non-error items, as >>({}).index?.error?.type !== 'version_conflict_engine_exception'
<< true but In doubt, I added more steps for the sake of readability. |
||
|
||
if (errors.length === 0) { | ||
return Either.right('bulk_index_succeeded' as const); | ||
} else { | ||
if (errors.every(isWriteBlockException)) { | ||
return Either.left({ | ||
type: 'target_index_had_write_block' as const, | ||
}); | ||
} | ||
Comment on lines
+85
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's very likely that if any write_block exception is encountered, all the objects encountered it, but just in case another error was returned, we check that all errors are effectively write block exceptions. |
||
throw new Error(JSON.stringify(errors)); | ||
} | ||
}) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
import { isIncompatibleMappingException, isWriteBlockException } from './es_errors'; | ||
|
||
describe('isWriteBlockError', () => { | ||
it('returns true for a `index write` cluster_block_exception', () => { | ||
expect( | ||
isWriteBlockException({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can add an integration test instead? Since it's easy to reproduce for an ES instance. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is already an integration test for the action using the function, but it doesn't hurt to do it for the helper itself. |
||
type: 'cluster_block_exception', | ||
reason: `index [.kibana_dolly] blocked by: [FORBIDDEN/8/index write (api)]`, | ||
}) | ||
).toEqual(true); | ||
}); | ||
it('returns true for a `moving to block index write` cluster_block_exception', () => { | ||
expect( | ||
isWriteBlockException({ | ||
type: 'cluster_block_exception', | ||
reason: `index [.kibana_dolly] blocked by: [FORBIDDEN/8/moving to block index write (api)]`, | ||
}) | ||
).toEqual(true); | ||
}); | ||
it('returns false for incorrect type', () => { | ||
expect( | ||
isWriteBlockException({ | ||
type: 'not_a_cluster_block_exception_at_all', | ||
reason: `index [.kibana_dolly] blocked by: [FORBIDDEN/8/index write (api)]`, | ||
}) | ||
).toEqual(false); | ||
}); | ||
}); | ||
|
||
describe('isIncompatibleMappingExceptionError', () => { | ||
it('returns true for `strict_dynamic_mapping_exception` errors', () => { | ||
expect( | ||
isIncompatibleMappingException({ | ||
type: 'strict_dynamic_mapping_exception', | ||
reason: 'idk', | ||
}) | ||
).toEqual(true); | ||
}); | ||
|
||
it('returns true for `mapper_parsing_exception` errors', () => { | ||
expect( | ||
isIncompatibleMappingException({ | ||
type: 'mapper_parsing_exception', | ||
reason: 'idk', | ||
}) | ||
).toEqual(true); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
export interface EsErrorCause { | ||
type: string; | ||
reason: string; | ||
} | ||
|
||
export const isWriteBlockException = ({ type, reason }: EsErrorCause): boolean => { | ||
return ( | ||
type === 'cluster_block_exception' && | ||
reason.match(/index \[.+] blocked by: \[FORBIDDEN\/8\/.+ \(api\)\]/) !== null | ||
); | ||
}; | ||
Comment on lines
+14
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Depending on the type of operation, the reason identifying a write block can vary e.g
I extracted the function previously present in |
||
|
||
export const isIncompatibleMappingException = ({ type }: EsErrorCause): boolean => { | ||
return type === 'strict_dynamic_mapping_exception' || type === 'mapper_parsing_exception'; | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -188,7 +188,14 @@ describe('migration actions', () => { | |||||||||||||||||||||||
transformedDocs: sourceDocs, | ||||||||||||||||||||||||
refresh: 'wait_for', | ||||||||||||||||||||||||
})() | ||||||||||||||||||||||||
).rejects.toMatchObject(expect.anything()); | ||||||||||||||||||||||||
).resolves.toMatchInlineSnapshot(` | ||||||||||||||||||||||||
Object { | ||||||||||||||||||||||||
"_tag": "Left", | ||||||||||||||||||||||||
"left": Object { | ||||||||||||||||||||||||
"type": "target_index_had_write_block", | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
`); | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not fan of snapshots to test resolved results (especially as the resolved object is small), but this is what is done in the other tests of the file, and I didn't want to fix them all in this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not for all the tests kibana/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts Lines 134 to 144 in 5cbb075
|
||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
it('resolves left index_not_found_exception when the index does not exist', async () => { | ||||||||||||||||||||||||
expect.assertions(1); | ||||||||||||||||||||||||
|
@@ -1496,7 +1503,7 @@ describe('migration actions', () => { | |||||||||||||||||||||||
} | ||||||||||||||||||||||||
`); | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
it('rejects if there are errors', async () => { | ||||||||||||||||||||||||
it('resolves left if there are write_block errors', async () => { | ||||||||||||||||||||||||
Comment on lines
-1499
to
+1503
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to also add a IT test to assert that it still rejects for other errors, but it seems that using an non-existing index surprisingly leads to a timeout (which is handled by If anyone has an idea, I'll take it. Else it's probably fine as it's covered in unit tests anyway. |
||||||||||||||||||||||||
const newDocs = ([ | ||||||||||||||||||||||||
{ _source: { title: 'doc 5' } }, | ||||||||||||||||||||||||
{ _source: { title: 'doc 6' } }, | ||||||||||||||||||||||||
|
@@ -1509,7 +1516,14 @@ describe('migration actions', () => { | |||||||||||||||||||||||
transformedDocs: newDocs, | ||||||||||||||||||||||||
refresh: 'wait_for', | ||||||||||||||||||||||||
})() | ||||||||||||||||||||||||
).rejects.toMatchObject(expect.anything()); | ||||||||||||||||||||||||
).resolves.toMatchInlineSnapshot(` | ||||||||||||||||||||||||
Object { | ||||||||||||||||||||||||
"_tag": "Left", | ||||||||||||||||||||||||
"left": Object { | ||||||||||||||||||||||||
"type": "target_index_had_write_block", | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
`); | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We forgot to update the RFC for the client-side reindex. Fixed it, and added the new
target_index_had_write_block
special case,