-
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
Prevent legacy url conflicts #116007
Prevent legacy url conflicts #116007
Conversation
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.
Preliminary review, overall this is looking good, and the unit test coverage is great. The logic is a bit hard to follow, especially with the amount of isLeft
and isRight
logic branches we have here. I had to pick this up and put it down a couple of times to fully understand what was going on.
I noticed that we didn't change the upsert
functionality within the repository's update
function -- is that intentional, or should we also be preventing legacy URL conflicts when an upsert
request results in a create
?
src/core/server/saved_objects/service/lib/preflight_check_for_create.ts
Outdated
Show resolved
Hide resolved
src/core/server/saved_objects/service/lib/preflight_check_for_create.ts
Outdated
Show resolved
Hide resolved
Had to make a small tweak to preflightCheckForCreate because the existing implementation caused a 500 error. Since the bulk-get call is optimized to only retrieve the `legacy-url-alias.disabled` field of the raw alias documents, if the `disabled` field does not exist in the raw document then the entire source object of the result is empty.
50e0c52
to
286d3ff
Compare
Yeah, I just fixed an upsert bug last week, not sure how I overlooked this. I think we should change |
src/core/server/saved_objects/service/lib/preflight_check_for_create.ts
Outdated
Show resolved
Hide resolved
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.
Overall looking good for now.
I guess it's not totally done though, given #116007 (comment)?
} catch (e) { | ||
error = e; | ||
} |
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.
Should we accumulate the errors, or exit the loop early when an error is encountered? ATM only the last error will be reported, but we're not failing fast when encountering errors, which seems wrong?
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.
I'm not sure I follow your concern. The whole loop is enclosed in the try/catch, so one error will break the loop. The only reason we have this catch is so that we can attempt to close the finder if we encountered an error. I don't think we need an accumulator, we just need to close the finder and throw the error that we encountered.
Can you clarify with a diff of your suggested changes?
*/ | ||
export async function findLegacyUrlAliases( | ||
createPointInTimeFinder: CreatePointInTimeFinderFn, | ||
objects: Array<{ type: string; id: string }> |
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.
NIT: Should we re-use SavedObjectsCollectMultiNamespaceReferencesObject
or a similar type? We have 99 { type: string; id: string }
type definitions, and the same amount of inlined { type: string; id: string }
.
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.
Well this is used for more than collectMultiNamespaceReferences
now. I can add a new type for this, though.
Just out of curiosity, are these type definitions a performance issue? Or just the principle of the thing?
export function getObjectKey({ type, id }: { type: string; id: string }) { | ||
return `${type}:${id}`; | ||
} |
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.
🙈 (I know this was already present)
export const getObjKey = (obj: SavedObject) => `${obj.type}|${obj.id}`; |
Same logic, different separator.
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.
Changed in 86ebd8c
Edit: this change was causing tests to fail, I reverted it in 327de44. I think this is fine to leave as-is.
export type CreatePointInTimeFinderFn = <T = unknown, A = unknown>( | ||
findOptions: SavedObjectsCreatePointInTimeFinderOptions | ||
) => ISavedObjectsPointInTimeFinder<T, A>; | ||
|
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.
I only see this used in added code. I wonder if we have existing code that should use this new type?
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.
Good question! I have already changed collect_multi_namespace_references.ts
to use it too. I looked around and didn't see anything else that looks like it could use this type.
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.
My biggest question is around the performance here. I kind of doubt that we have places in Kibana that create large batches across many namespaces. So I assume performance isn't critical, but I think it's still worth understanding the worst case performance.
* If the object does not exceed this threshold, we use mget to fetch aliases instead. | ||
* This threshold is a bit arbitrary, but it is intended to optimize so we don't make expensive PIT/find operations unless it is necessary. | ||
*/ | ||
const FIND_ALIASES_THRESHOLD = 3; |
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 should probably introduce a limit to the bulk create API to make it easier to reason about the possible performance.
If we assume a limit of 10k saved objects per batch and all (or the majority) of objects are only created in 2 spaces it would create an mget
with number_of_spaces*object_count + object_count
so trying to get 30K _id's which feels very inefficient, but maybe it's anyway performant.
If this bulk create has just one object with 4 namespaces we will also create a PIT and do a find in addition to this large mget.
Since the performance tradeoff is likely between how large the mget request becomes vs the time to create a PIT and page through results I think we should select just one strategy per batch and the heuristic should be the number of _id's in the mget for the entire bulk instead of the number of spaces per object.
So we would loop through all objects and sum up the amount of spaces that each object will be created in. And if we end up with e.g. > 1000
potential legacy url alias id's that need to be mget'd we switch to using PIT/find (then we could try to profile it and see if 1000
is the right threshold or not)
If there's one object with a *
namespace we could let the entire batch fall back to PIT/find.
(this might also simplify the code since we're not having to create so many discriminated unions with a left and right that has a different meaning in different contexts)
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.
This implementation is just a first stab at optimizing performance and I agree it surely can be improved. I went into this with the primary assumption that it's generally more performant to fetch aliases with mget
than find
.
I guess the big variable here is, how many aliases do we actually have?
Here's my concern:
Assume that we have Kibana 7.16 with 1k objects in each of 100 spaces (100k objects total). You upgrade Kibana to 8.0, which means you also have 99k aliases.
If you do a bulkCreate for 1k objects, and it includes just one object with 4 namespaces:
- With the current implementation: we create a PIT and do a
find
for one alias, then domget
for 1999 documents (1k objects + 999 aliases) - With your suggestion: we create a PIT and do a
find
for all aliases, we have to page through 99k search results, then domget
for 1000 documents
Aside from that: when using find
, we have to add a new compound query clause for each object. I wonder if Elasticsearch could handle a single find
with 1k or 10k such clauses? I haven't done any testing, I guess we could find out.
Given that most object types aren't shareable yet, I'm thinking we don't have to worry about these performance edge cases just yet. Do you feel comfortable with me/us tackling this in a follow-on (perhaps as part of #113743)?
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.
Yeah happy to defer performance to a follow-up.
Consumers can now define how many aliases they want to search for per page: * collectMultiNamespaceReferences uses 100 because it's less important * preflightCheckForCreate uses 1000 because it's more important and it could be paging through potentially many thousands of results
The current implementation causes an extra round trip to ES if a new object would be created, but given the infrequency of the affected functions and our current time constraints, it seems acceptable because it is the simplest path forward.
I updated this to include an alias check when an upsert would create a new object (during |
if (upsert && preflightResult.checkResult === 'not_found') { | ||
// If an upsert would result in the creation of a new object, we need to check for alias conflicts too. | ||
// This takes an extra round trip to Elasticsearch, but this won't happen often. | ||
// TODO: improve performance by combining these into a single preflight check | ||
await this.preflightCheckForUpsertAliasConflict(type, id, namespace); | ||
} |
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.
I really don't like having an extra preflight check, but I think that it's reasonable for now given our timetable and the low frequency of upsert operations. I'd like to revisit this as part of #113743 too.
It turns out this is exposed to external consumers and caused some integration tests to fail. The unrelated saved objects service getObjKey function is internal only and uses a different delimiter.
Also updated dev docs.
💛 Build succeeded, but was flaky
Test Failures
Metrics [docs]
History
To update your PR or re-run it, just comment with: |
I believe the flaky test above is unrelated to the changes in this PR. Instead, it looks like it could be related to this open issue: #115303 |
Resolves #113335.
See commit messages for details, this involved some refactoring so it may be easier to review on a per-commit basis.
I changed the SOR
create
/bulkCreate
methods to use a newpreflightCheckForCreate
function. From the TS docs:TODO: address upsert inEdit: done in this PRupdate
andincrementCounter
(see #116007 (comment))In creating this PR, I identified a few other situations where a legacy URL alias conflict scenario could be created, which this PR does not account for. TODO (in a follow up bugfix PR):
Delete aliases when objects are deleted?Delete aliases when objects are unshared?^ Basically if there is ever an object change that causes an alias target to point to nothing, we should probably just delete the alias. That's what I 'm thinking right now.Update: tracking this in a separate issue, #116235